001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.objecttools;
018
019import static org.apache.juneau.common.utils.StringUtils.*;
020
021import java.util.*;
022import java.util.regex.*;
023
024import org.apache.juneau.*;
025import org.apache.juneau.common.utils.*;
026
027/**
028 * String matcher factory for the {@link ObjectSearcher} class.
029 *
030 * <p>
031 *    The class provides searching based on the following patterns:
032 * </p>
033 * <ul>
034 *    <li><js>"property=foo"</js> - Simple full word match
035 *    <li><js>"property=fo*"</js>, <js>"property=?ar"</js> - Meta-character matching
036 *    <li><js>"property=foo bar"</js>(implicit), <js>"property=^foo ^bar"</js>(explicit) - Multiple OR'ed patterns
037 *    <li><js>"property=+fo* +*ar"</js> - Multiple AND'ed patterns
038 *    <li><js>"property=fo* -bar"</js> - Negative patterns
039 *    <li><js>"property='foo bar'"</js> - Patterns with whitespace
040 *    <li><js>"property=foo\\'bar"</js> - Patterns with single-quotes
041 *    <li><js>"property=/foo\\s+bar"</js> - Regular expression match
042 * </ul>
043 *
044 * <h5 class='section'>See Also:</h5><ul>
045 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/ObjectTools">Object Tools</a>
046 * </ul>
047 */
048public class StringMatcherFactory extends MatcherFactory {
049
050   /**
051    * Default reusable matcher.
052    */
053   public static final StringMatcherFactory DEFAULT = new StringMatcherFactory();
054
055   @Override
056   public boolean canMatch(ClassMeta<?> cm) {
057      return true;
058   }
059
060   @Override
061   public AbstractMatcher create(String pattern) {
062      return new StringMatcher(pattern);
063   }
064
065   /**
066    * A construct representing a single search pattern.
067    */
068   private static class StringMatcher extends AbstractMatcher {
069      private String pattern;
070      private static final AsciiSet
071         META_CHARS = AsciiSet.of("*?'\""),
072         SQ_CHAR = AsciiSet.of("'"),
073         DQ_CHAR = AsciiSet.of("\""),
074         REGEX_CHARS = AsciiSet.of("+\\[]{}()^$.");
075
076      Pattern[] orPatterns, andPatterns, notPatterns;
077
078      public StringMatcher(String searchPattern) {
079
080         this.pattern = searchPattern.trim();
081         List<Pattern> ors = new LinkedList<>();
082         List<Pattern> ands = new LinkedList<>();
083         List<Pattern> nots = new LinkedList<>();
084
085         for (String s : Utils.splitQuoted(pattern, true)) {
086            char c0 = s.charAt(0), c9 = s.charAt(s.length()-1);
087
088            if (c0 == '/' && c9 == '/' && s.length() > 1) {
089               ands.add(Pattern.compile(strip(s)));
090            } else {
091               char prefix = '^';
092               boolean ignoreCase = false;
093               if (s.length() > 1 && (c0 == '^' || c0 == '+' || c0 == '-')) {
094                  prefix = c0;
095                  s = s.substring(1);
096                  c0 = s.charAt(0);
097               }
098
099               if (c0 == '\'') {
100                  s = unEscapeChars(strip(s), SQ_CHAR);
101                  ignoreCase = true;
102               } else if (c0 == '"') {
103                  s = unEscapeChars(strip(s), DQ_CHAR);
104               }
105
106               if (REGEX_CHARS.contains(s) || META_CHARS.contains(s)) {
107                  StringBuilder sb = new StringBuilder();
108                  boolean isInEscape = false;
109                  for (int i = 0; i < s.length(); i++) {
110                     char c = s.charAt(i);
111                     if (isInEscape) {
112                        if (c == '?' || c == '*' || c == '\\')
113                           sb.append('\\').append(c);
114                        else
115                           sb.append(c);
116                        isInEscape = false;
117                     } else {
118                        if (c == '\\')
119                           isInEscape = true;
120                        else if (c == '?')
121                           sb.append(".?");
122                        else if (c == '*')
123                           sb.append(".*");
124                        else if (REGEX_CHARS.contains(c))
125                           sb.append("\\").append(c);
126                        else
127                           sb.append(c);
128                     }
129                  }
130                  s = sb.toString();
131               }
132
133
134               int flags = Pattern.DOTALL;
135               if (ignoreCase)
136                  flags |= Pattern.CASE_INSENSITIVE;
137
138               Pattern p = Pattern.compile(s, flags);
139
140               if (prefix == '-')
141                  nots.add(p);
142               else if (prefix == '+')
143                  ands.add(p);
144               else
145                  ors.add(p);
146            }
147         }
148         orPatterns = ors.toArray(new Pattern[ors.size()]);
149         andPatterns = ands.toArray(new Pattern[ands.size()]);
150         notPatterns = nots.toArray(new Pattern[nots.size()]);
151      }
152
153      @Override
154      public boolean matches(ClassMeta<?> cm, Object o) {
155         String s = (String)o;
156         for (Pattern andPattern : andPatterns)
157                if (! andPattern.matcher(s).matches())
158               return false;
159         for (Pattern notPattern : notPatterns)
160                if (notPattern.matcher(s).matches())
161               return false;
162         for (Pattern orPattern : orPatterns)
163                if (orPattern.matcher(s).matches())
164               return true;
165         return orPatterns.length == 0;
166      }
167
168      @Override
169      public String toString() {
170         return pattern;
171      }
172   }
173}