001/* 002 * Shredzone Commons 003 * 004 * Copyright (C) 2012 Richard "Shred" Körber 005 * http://commons.shredzone.org 006 * 007 * This program is free software: you can redistribute it and/or modify 008 * it under the terms of the GNU Library General Public License as 009 * published by the Free Software Foundation, either version 3 of the 010 * License, or (at your option) any later version. 011 * 012 * This program is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 015 * GNU General Public License for more details. 016 * 017 * You should have received a copy of the GNU Library General Public License 018 * along with this program. If not, see <http://www.gnu.org/licenses/>. 019 */ 020package org.shredzone.commons.text.filter; 021 022import static java.util.Comparator.comparing; 023import static java.util.stream.Collectors.joining; 024 025import java.util.HashMap; 026import java.util.Map; 027import java.util.Objects; 028import java.util.regex.Matcher; 029import java.util.regex.Pattern; 030 031import edu.umd.cs.findbugs.annotations.Nullable; 032import org.shredzone.commons.text.TextFilter; 033 034/** 035 * A filter that detects smily sequences, and replaces them with an image. The filter 036 * tries to find the best match by the string length of the smily code, so it can safely 037 * distinguish between smilies like ":-)" and ":-))". 038 * 039 * @author Richard "Shred" Körber 040 */ 041public class SmilyFilter implements TextFilter { 042 043 private String baseUrl = ""; 044 private final Map<String, String> smilyMap = new HashMap<>(); 045 private @Nullable Pattern smilyPattern; 046 047 /** 048 * Creates a new {@link SmilyFilter}. 049 */ 050 public SmilyFilter() { 051 updatePattern(); 052 } 053 054 /** 055 * Adds a smily to be detected. 056 * 057 * @param smily 058 * Smily code to detect (e.g. ":-)") 059 * @param image 060 * Image file name to be shown instead 061 */ 062 public void addSmily(String smily, String image) { 063 smilyMap.put(Objects.requireNonNull(smily), Objects.requireNonNull(image)); 064 updatePattern(); 065 } 066 067 /** 068 * Sets the base url that is prepended to the image file names. 069 * 070 * @param url 071 * Base url (e.g. "/img/smiles"), defaults to the current directory 072 */ 073 public void setBaseUrl(String url) { 074 this.baseUrl = Objects.requireNonNull(url); 075 if (baseUrl.length() > 0 && !baseUrl.endsWith("/")) { 076 baseUrl += "/"; 077 } 078 } 079 080 /** 081 * Updates the regular expression for smily detection. 082 */ 083 private void updatePattern() { 084 // We need to sort the smilys by their string length (descending), so the regex 085 // will match the longest smilys first (":-))" before ":-)"). 086 String pattern = smilyMap.keySet().stream() 087 .sorted(comparing(String::length).reversed()) 088 .map(Pattern::quote) 089 .collect(joining("|")); 090 091 smilyPattern = Pattern.compile(pattern, Pattern.DOTALL); 092 } 093 094 private String escapeHtml(String text) { 095 return text.replace("&", "&").replace("<", "<").replace("\"", """); 096 } 097 098 @Override 099 public CharSequence apply(CharSequence text) { 100 if (smilyPattern == null) { 101 return text; 102 } 103 104 Matcher m = smilyPattern.matcher(text); 105 106 StringBuilder result = null; 107 int lastEnd = 0; 108 109 m.reset(); 110 while(m.find()) { 111 if (result == null) { 112 result = new StringBuilder(); 113 } 114 115 result.append(text, lastEnd, m.start()); 116 117 String smily = m.group(); 118 String smilyUrl = smilyMap.get(smily); 119 if (smilyUrl != null) { 120 result.append("<img src=\"").append(baseUrl).append(smilyUrl).append('"'); 121 // TODO: Add optional class/style and width/height attributes 122 result.append(" alt=\"").append(escapeHtml(smily)).append('"'); 123 result.append(" />"); 124 125 } else { 126 // Append the smily code. Should never happen, as the regex is built from 127 // the map keys... 128 result.append(smily); 129 } 130 131 lastEnd = m.end(); 132 } 133 134 if (result == null) { 135 return text; 136 } 137 138 if (lastEnd < text.length()) { 139 result.append(text, lastEnd, text.length()); 140 } 141 142 return result; 143 } 144 145}