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}