IrregularUnicodeBlockDetector.java

  1/*
  2 * Copyright (c) 2018, Daniel Gultsch All rights reserved.
  3 *
  4 * Redistribution and use in source and binary forms, with or without modification,
  5 * are permitted provided that the following conditions are met:
  6 *
  7 * 1. Redistributions of source code must retain the above copyright notice, this
  8 * list of conditions and the following disclaimer.
  9 *
 10 * 2. Redistributions in binary form must reproduce the above copyright notice,
 11 * this list of conditions and the following disclaimer in the documentation and/or
 12 * other materials provided with the distribution.
 13 *
 14 * 3. Neither the name of the copyright holder nor the names of its contributors
 15 * may be used to endorse or promote products derived from this software without
 16 * specific prior written permission.
 17 *
 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 19 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 20 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 21 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
 22 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 23 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 24 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 25 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 26 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 27 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 28 */
 29
 30package eu.siacs.conversations.utils;
 31
 32import android.content.Context;
 33import android.support.annotation.ColorInt;
 34import android.text.Spannable;
 35import android.text.SpannableString;
 36import android.text.SpannableStringBuilder;
 37import android.text.style.ForegroundColorSpan;
 38import android.util.LruCache;
 39
 40import java.util.ArrayList;
 41import java.util.Collections;
 42import java.util.HashMap;
 43import java.util.HashSet;
 44import java.util.List;
 45import java.util.Map;
 46import java.util.Set;
 47import java.util.regex.Matcher;
 48import java.util.regex.Pattern;
 49
 50import eu.siacs.conversations.R;
 51import eu.siacs.conversations.ui.util.Color;
 52import rocks.xmpp.addr.Jid;
 53
 54public class IrregularUnicodeBlockDetector {
 55
 56	private static final Map<Character.UnicodeBlock,Character.UnicodeBlock> NORMALIZATION_MAP;
 57
 58	static {
 59		Map<Character.UnicodeBlock,Character.UnicodeBlock> temp = new HashMap<>();
 60		temp.put(Character.UnicodeBlock.LATIN_1_SUPPLEMENT, Character.UnicodeBlock.BASIC_LATIN);
 61		NORMALIZATION_MAP = Collections.unmodifiableMap(temp);
 62	}
 63
 64	private static Character.UnicodeBlock normalize(Character.UnicodeBlock in) {
 65		if (NORMALIZATION_MAP.containsKey(in)) {
 66			return NORMALIZATION_MAP.get(in);
 67		} else {
 68			return in;
 69		}
 70	}
 71
 72	private static final LruCache<Jid, Pattern> CACHE = new LruCache<>(100);
 73
 74	public static Spannable style(Context context, Jid jid) {
 75		return style(jid, Color.get(context, R.attr.color_warning));
 76	}
 77
 78	private static Spannable style(Jid jid, @ColorInt int color) {
 79		SpannableStringBuilder builder = new SpannableStringBuilder();
 80		if (jid.getLocal() != null) {
 81			SpannableString local = new SpannableString(jid.getLocal());
 82			Matcher matcher = find(jid).matcher(local);
 83			while (matcher.find()) {
 84				if (matcher.start() < matcher.end()) {
 85					local.setSpan(new ForegroundColorSpan(color), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 86				}
 87			}
 88			builder.append(local);
 89			builder.append('@');
 90		}
 91		if (jid.getDomain() != null) {
 92			builder.append(jid.getDomain());
 93		}
 94		if (builder.length() != 0 && jid.getResource() != null) {
 95			builder.append('/');
 96			builder.append(jid.getResource());
 97		}
 98		return builder;
 99	}
100
101	private static Map<Character.UnicodeBlock, List<String>> map(Jid jid) {
102		Map<Character.UnicodeBlock, List<String>> map = new HashMap<>();
103		String local = jid.getLocal();
104		final int length = local.length();
105		for (int offset = 0; offset < length; ) {
106			final int codePoint = local.codePointAt(offset);
107			Character.UnicodeBlock block = normalize(Character.UnicodeBlock.of(codePoint));
108			List<String> codePoints;
109			if (map.containsKey(block)) {
110				codePoints = map.get(block);
111			} else {
112				codePoints = new ArrayList<>();
113				map.put(block, codePoints);
114			}
115			codePoints.add(String.copyValueOf(Character.toChars(codePoint)));
116			offset += Character.charCount(codePoint);
117		}
118		return map;
119	}
120
121	private static Set<String> eliminateFirstAndGetCodePoints(Map<Character.UnicodeBlock, List<String>> map) {
122		Character.UnicodeBlock block = Character.UnicodeBlock.BASIC_LATIN;
123		int size = 0;
124		for (Map.Entry<Character.UnicodeBlock, List<String>> entry : map.entrySet()) {
125			if (entry.getValue().size() > size) {
126				size = entry.getValue().size();
127				block = entry.getKey();
128			}
129		}
130		map.remove(block);
131		Set<String> all = new HashSet<>();
132		for (List<String> codePoints : map.values()) {
133			all.addAll(codePoints);
134		}
135		return all;
136	}
137
138	private static Pattern find(Jid jid) {
139		synchronized (CACHE) {
140			Pattern pattern = CACHE.get(jid);
141			if (pattern != null) {
142				return pattern;
143			}
144			pattern = create(eliminateFirstAndGetCodePoints(map(jid)));
145			CACHE.put(jid, pattern);
146			return pattern;
147		}
148	}
149
150	private static Pattern create(Set<String> codePoints) {
151		final StringBuilder pattern = new StringBuilder();
152		for (String codePoint : codePoints) {
153			if (pattern.length() != 0) {
154				pattern.append('|');
155			}
156			pattern.append(Pattern.quote(codePoint));
157		}
158		return Pattern.compile(pattern.toString());
159	}
160}