MongoNamePattern.java

package com.dbschema.mongo;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.HashSet;
import java.util.Set;
import java.util.regex.Pattern;

public final class MongoNamePattern {
  private static final Set<Character> SPECIAL_REGEX_CHARS = new HashSet<>();

  private final String plain;
  private final Pattern pattern;

  static {
    SPECIAL_REGEX_CHARS.add('[');
    SPECIAL_REGEX_CHARS.add(']');
    SPECIAL_REGEX_CHARS.add('{');
    SPECIAL_REGEX_CHARS.add('}');
    SPECIAL_REGEX_CHARS.add('(');
    SPECIAL_REGEX_CHARS.add(')');
    SPECIAL_REGEX_CHARS.add('\\');
    SPECIAL_REGEX_CHARS.add('.');
    SPECIAL_REGEX_CHARS.add('*');
    SPECIAL_REGEX_CHARS.add('+');
    SPECIAL_REGEX_CHARS.add('?');
    SPECIAL_REGEX_CHARS.add('^');
    SPECIAL_REGEX_CHARS.add('$');
    SPECIAL_REGEX_CHARS.add('|');
  }

  private MongoNamePattern() {
    this.plain = null;
    this.pattern = null;
  }

  private MongoNamePattern(@NotNull Pattern pattern) {
    this.plain = null;
    this.pattern = pattern;
  }

  private MongoNamePattern(@NotNull String plain) {
    this.plain = plain;
    this.pattern = null;
  }

  @Nullable
  public String asPlain() {
    return plain;
  }

  public boolean matches(@NotNull String name) {
    return plain != null ? plain.equals(name) :
           pattern == null || pattern.matcher(name).matches();
  }

  /**
   * Given an inputPattern using SQL syntax (e.g. % for wildcard, and '_' for single character) generate a Java Pattern
   * that can be used to validate input.
   *
   * @param inputPattern The String representing the SQL pattern.
   * @return A suitable Pattern if the input has wildcard characters in it, or NULL if no pattern matching required.
   */
  @NotNull
  public static MongoNamePattern create(String inputPattern) throws IllegalArgumentException {
    if (inputPattern == null) return new MongoNamePattern();
    String plain = toPlain(inputPattern);
    if (plain != null) return new MongoNamePattern(plain);

    boolean escaped = false;
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < inputPattern.length(); i++) {
      char c = inputPattern.charAt(i);
      if (c == '\\') {
        if (escaped) sb.append("\\\\");
        escaped = !escaped;
      }
      else if (escaped) {
        escaped = false;
        if (c == '_' || c == '%') sb.append(c);
        else
          throw new IllegalArgumentException("Illegal char escape " + c + " at position " + i + " string: " + inputPattern);
      }
      else {
        if (c == '_') sb.append(".");
        else if (c == '%') sb.append(".*");
        else if (SPECIAL_REGEX_CHARS.contains(c)) sb.append('\\').append(c);
        else sb.append(c);
      }
    }

    String pattern = sb.toString();
    return pattern.equals(".*") ? new MongoNamePattern() : new MongoNamePattern(Pattern.compile(pattern));
  }

  @Nullable
  private static String toPlain(@NotNull String inputPattern) {
    boolean escaped = false;
    StringBuilder plain = new StringBuilder();
    for (int i = 0; i < inputPattern.length(); i++) {
      char c = inputPattern.charAt(i);
      if (c == '\\') {
        if (escaped) plain.append("\\");
        escaped = !escaped;
      }
      else if (escaped) {
        escaped = false;
        if (c == '_') plain.append("_");
        else if (c == '%') plain.append("%");
        else
          throw new IllegalArgumentException("Illegal char escape " + c + " at position " + i + " string: " + inputPattern);
      }
      else {
        if (c == '_' || c == '%') return null; // unescaped pattern entities
        plain.append(c);
      }
    }
    return plain.toString();
  }
}