code: package net.codebox.homoglyph;
  
  import java.util.*;
  
  /**
   * Use this class to detect occurrences of target words inside a String, where the target words may have been
   * disguised using homoglyph substitution and/or by mixing upper/lower case letters (for example, the class
   * will find the word "credit" in the String "Free Ꮯгⴹԁ1ｔ").
   *
   * You can supply your own list of homoglyphs, or use the char_codes.txt which should accompany this source file.
   * You can find the latest version of char_codes.txt at https://github.com/codebox/homoglyph
   *
   * @author Rob Dawson
   */
  public class Homoglyph {
      private final List<Set<Integer>> homoglyphs = new ArrayList<>();
  
      private final CachingLookup cache = new CachingLookup();
  
      /**
       * Supply a List of Sets, with each Set containing a group of Unicode codepoints that are homoglyphs. Codepoints
       * must be represented using Integer rather than Character values because some are too large to be held by the
       * 16-bit Character type.
       *
       * @param homoglyphs a List of Sets, with each Set containing a group of Unicode codepoints that are homoglyphs
       */
      public Homoglyph(final List<Set<Integer>> homoglyphs) {
          this.homoglyphs.addAll(homoglyphs);
      }
  
      /**
       * Search the String {@code text} to locate all occurrences of the words contained in {@code targetWords},
       * accounting for homoglyph substitution and variations of case.
       *
       * @param text text to be searched
       * @param targetWords words to be located
       * @return a List containing the results of the search, if no matches were found an empty list will be returned
       */
      public List<SearchResult> search(final String text, final Collection<String> targetWords) {
          final List<SearchResult> allResults = new ArrayList<>();
  
          final CodePoints textCodepoints = new CodePoints(text);
  
          for (final String targetWord : targetWords) {
              allResults.addAll(checkForWord(textCodepoints, new CodePoints(targetWord)));
          }
  
          return allResults;
      }
  
      /**
       * Search the String {@code text} to locate all occurrences of the words contained in {@code targetWords},
       * accounting for homoglyph substitution and variations of case.
       *
       * @param text text to be searched
       * @param targetWords words to be located
       * @return a List containing the results of the search, if no matches were found an empty list will be returned
       */
      public List<SearchResult> search(final String text, final String... targetWords) {
          return search(text, Arrays.asList(targetWords));
      }
  
      private Collection<SearchResult> checkForWord(final CodePoints text, final CodePoints targetWord) {
          final Collection<SearchResult> results = new ArrayList<>();
  
          int lastIndex = text.getLength() - targetWord.getLength();
          for (int i = 0; i <= lastIndex; i++) {
              if (hasWordAtIndex(text, targetWord, i)) {
                  results.add(new SearchResult(i, text.subStringAt(i, targetWord.getLength()), targetWord.getText()));
              }
          }
  
          return results;
      }
  
      private boolean hasWordAtIndex(final CodePoints text, final CodePoints targetWord, final int index) {
          for (int i=0; i<targetWord.getLength(); i++) {
              final int targetCharLower = Character.toLowerCase(targetWord.getValue(i));
              final int targetCharUpper = Character.toUpperCase(targetWord.getValue(i));
              final int textChar = text.getValue(index + i);
              if (!checkForHomoglyphs(targetCharLower, textChar) && !checkForHomoglyphs(targetCharUpper, textChar)){
                  return false;
              }
          }
          return true;
      }
  
      private boolean checkForHomoglyphs(final int cp1, final int cp2) {
          final Set<Integer> cp1Set = cache.lookup(cp1);
          return cp1Set.contains(cp2);
      }
  
      public static class SearchResult {
          public SearchResult(final int index, final String match, final String word){
              this.index = index;
              this.match = match;
              this.word = word;
          }
          public int index;
          public String match;
          public String word;
  
          @Override
          public String toString() {
              return String.format("'%s' at position %s matches '%s'", match, index, word);
          }
      }
  
      public static class CodePoints {
          private final Integer[] codepoints;
          private final String text;
  
          public CodePoints(String text){
              this.text = text;
  
              final List<Integer> codepointList = new ArrayList<>();
              int codepoint;
              for (int offset = 0; offset < text.length(); ) {
                  codepointList.add(codepoint = text.codePointAt(offset));
                  offset += Character.charCount(codepoint);
              }
              codepoints = codepointList.toArray(new Integer[0]);
          }
  
          public int getValue(int i) {
              return codepoints[i];
          }
  
          public int getLength() {
              return codepoints.length;
          }
  
          public String getText() {
              return text;
          }
  
          public String subStringAt(final int s, final int l) {
              final StringBuilder sb = new StringBuilder(l);
              for (int i=0; i<l; i++){
                  sb.appendCodePoint(this.codepoints[s+i]);
              }
              return sb.toString();
          }
      }
  
      public class CachingLookup {
          private final Map<Integer, Set<Integer>> lookup = new HashMap<>();
  
          public Set<Integer> lookup(final int cp) {
              Set<Integer> s = lookup.get(cp);
              if (s == null){
                  for (Set<Integer> thisSet : homoglyphs){
                      if (thisSet.contains(cp)){
                          s = thisSet;
                          break;
                      }
                  }
                  if (s == null){
                      s = new HashSet<>();
                      s.add(cp);
                  }
                  lookup.put(cp, s);
              }
              return s;
          }
      }
  
  }