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("&", "&amp;").replace("<", "&lt;").replace("\"", "&quot;");
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}