ExoPattern.java

/*
 * Copyright (C) 2003-2012 eXo Platform SAS.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
package org.exoplatform.social.common.router.regex;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ExoPattern {

  // Group pattern which uses to match Group /{group}/
  private static final Pattern            GROUP_PATTERN = Pattern.compile("\\(\\{(\\w+)\\}");

  private Pattern                         pattern;

  private String                          namedPattern;

  private Map<String, List<ExoGroupData>> groupInfo;

  public static ExoPattern compile(String regex) {
    return new ExoPattern(regex, 0);
  }

  public static ExoPattern compile(String regex, int flags) {
    return new ExoPattern(regex, flags);
  }

  private ExoPattern(String regex, int flags) {
    namedPattern = regex;
    pattern = buildStandardPattern(regex, flags);
    groupInfo = extractGroupInfo(regex);
  }

  public int indexOf(String groupName) {
    return indexOf(groupName, 0);
  }

  public int indexOf(String groupName, int index) {
    int idx = -1;
    if (groupInfo.containsKey(groupName)) {
      List<ExoGroupData> list = groupInfo.get(groupName);
      if (index < list.size()) {
        idx = list.get(index).getGroupIndex();
      }
    }
    return idx;
  }

  public ExoMatcher matcher(CharSequence input) {
    return new ExoMatcherImpl(this, input);
  }

  public boolean matches(String s) {
    return pattern.matcher(s).matches();
  }

  public Pattern pattern() {
    return pattern;
  }

  public String toString() {
    return namedPattern;
  }

  static private boolean isEscapedParent(String s, int pos) {
    int numSlashes = 0;
    while (pos > 0 && (s.charAt(pos - 1) == '\\')) {
      pos--;
      numSlashes++;
    }
    return numSlashes % 2 != 0;
  }

  static private boolean isNoncapturingParent(String s, int pos) {
    int len = s.length();
    boolean isLookbehind = false;
    if (pos >= 0 && pos + 4 < len) {
      String pre = s.substring(pos, pos + 4);
      isLookbehind = pre.equals("(?<=") || pre.equals("(?<!");
    }
    return (pos >= 0 && pos + 2 < len) && s.charAt(pos + 1) == '?'
        && (isLookbehind || s.charAt(pos + 2) != '<');
  }

  static private int countOpenParents(String s, int pos) {
    Pattern p = Pattern.compile("\\(");
    Matcher m = p.matcher(s.subSequence(0, pos));

    int numParens = 0;

    while (m.find()) {
      String match = m.group(0);

      // ignore escaped parents
      if (isEscapedParent(s, m.start()))
        continue;

      if (match.equals("(") && !isNoncapturingParent(s, m.start())) {
        numParens++;
      }
    }
    return numParens;
  }

  /**
   * Process extract given pattern string to Map of ArgumentName and GroupData
   * 
   * @param namedPattern
   * @return
   */
  static public Map<String, List<ExoGroupData>> extractGroupInfo(String namedPattern) {
    Map<String, List<ExoGroupData>> groupInfo = new LinkedHashMap<String, List<ExoGroupData>>();
    Matcher matcher = GROUP_PATTERN.matcher(namedPattern);
    while (matcher.find()) {

      int pos = matcher.start();

      // ignore escaped parent
      if (isEscapedParent(namedPattern, pos))
        continue;

      String name = matcher.group(1);
      int groupIndex = countOpenParents(namedPattern, pos);

      List<ExoGroupData> list;
      if (groupInfo.containsKey(name)) {
        list = groupInfo.get(name);
      } else {
        list = new ArrayList<ExoGroupData>();
      }
      list.add(new ExoGroupData(groupIndex, pos));
      groupInfo.put(name, list);
    }
    return groupInfo;
  }

  static private Pattern buildStandardPattern(String namedPattern, Integer flags) {

    StringBuilder s = new StringBuilder(namedPattern);
    Matcher m = GROUP_PATTERN.matcher(s);
    while (m.find()) {
      int start = m.start();
      int end = m.end();

      if (isEscapedParent(s.toString(), start)) {
        continue;
      }

      s.replace(start, end, "(");
      m.reset();
    }

    return Pattern.compile(s.toString(), flags);
  }

  static private class ExoGroupData {
    private int pos;

    private int groupIndex;

    ExoGroupData(int groupIndex, int pos) {
      this.groupIndex = groupIndex;
      this.pos = pos;
    }

    public int getGroupIndex() {
      return groupIndex;
    }

    public int getPos() {
      return pos;
    }

  }

}