1 | /* |
2 | * Copyright 2006-2007 the original author or authors. |
3 | * |
4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
5 | * you may not use this file except in compliance with the License. |
6 | * You may obtain a copy of the License at |
7 | * |
8 | * http://www.apache.org/licenses/LICENSE-2.0 |
9 | * |
10 | * Unless required by applicable law or agreed to in writing, software |
11 | * distributed under the License is distributed on an "AS IS" BASIS, |
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
13 | * See the License for the specific language governing permissions and |
14 | * limitations under the License. |
15 | */ |
16 | package org.springframework.batch.support; |
17 | |
18 | import java.util.ArrayList; |
19 | import java.util.Collections; |
20 | import java.util.Comparator; |
21 | import java.util.HashMap; |
22 | import java.util.List; |
23 | import java.util.Map; |
24 | |
25 | import org.springframework.util.Assert; |
26 | |
27 | /** |
28 | * @author Dave Syer |
29 | * @author Dan Garrette |
30 | */ |
31 | public class PatternMatcher<S> { |
32 | |
33 | private Map<String, S> map = new HashMap<String, S>(); |
34 | private List<String> sorted = new ArrayList<String>(); |
35 | |
36 | /** |
37 | * Initialize a new {@link PatternMatcher} with a map of patterns to values |
38 | * @param map a map from String patterns to values |
39 | */ |
40 | public PatternMatcher(Map<String, S> map) { |
41 | super(); |
42 | this.map = map; |
43 | // Sort keys to start with the most specific |
44 | sorted = new ArrayList<String>(map.keySet()); |
45 | Collections.sort(sorted, new Comparator<String>() { |
46 | public int compare(String o1, String o2) { |
47 | String s1 = o1; // .replace('?', '{'); |
48 | String s2 = o2; // .replace('*', '}'); |
49 | return s2.compareTo(s1); |
50 | } |
51 | }); |
52 | } |
53 | |
54 | /** |
55 | * Lifted from AntPathMatcher in Spring Core. Tests whether or not a string |
56 | * matches against a pattern. The pattern may contain two special |
57 | * characters:<br> |
58 | * '*' means zero or more characters<br> |
59 | * '?' means one and only one character |
60 | * |
61 | * @param pattern pattern to match against. Must not be <code>null</code>. |
62 | * @param str string which must be matched against the pattern. Must not be |
63 | * <code>null</code>. |
64 | * @return <code>true</code> if the string matches against the pattern, or |
65 | * <code>false</code> otherwise. |
66 | */ |
67 | public static boolean match(String pattern, String str) { |
68 | char[] patArr = pattern.toCharArray(); |
69 | char[] strArr = str.toCharArray(); |
70 | int patIdxStart = 0; |
71 | int patIdxEnd = patArr.length - 1; |
72 | int strIdxStart = 0; |
73 | int strIdxEnd = strArr.length - 1; |
74 | char ch; |
75 | |
76 | boolean containsStar = pattern.contains("*"); |
77 | |
78 | if (!containsStar) { |
79 | // No '*'s, so we make a shortcut |
80 | if (patIdxEnd != strIdxEnd) { |
81 | return false; // Pattern and string do not have the same size |
82 | } |
83 | for (int i = 0; i <= patIdxEnd; i++) { |
84 | ch = patArr[i]; |
85 | if (ch != '?') { |
86 | if (ch != strArr[i]) { |
87 | return false;// Character mismatch |
88 | } |
89 | } |
90 | } |
91 | return true; // String matches against pattern |
92 | } |
93 | |
94 | if (patIdxEnd == 0) { |
95 | return true; // Pattern contains only '*', which matches anything |
96 | } |
97 | |
98 | // Process characters before first star |
99 | while ((ch = patArr[patIdxStart]) != '*' && strIdxStart <= strIdxEnd) { |
100 | if (ch != '?') { |
101 | if (ch != strArr[strIdxStart]) { |
102 | return false;// Character mismatch |
103 | } |
104 | } |
105 | patIdxStart++; |
106 | strIdxStart++; |
107 | } |
108 | if (strIdxStart > strIdxEnd) { |
109 | // All characters in the string are used. Check if only '*'s are |
110 | // left in the pattern. If so, we succeeded. Otherwise failure. |
111 | for (int i = patIdxStart; i <= patIdxEnd; i++) { |
112 | if (patArr[i] != '*') { |
113 | return false; |
114 | } |
115 | } |
116 | return true; |
117 | } |
118 | |
119 | // Process characters after last star |
120 | while ((ch = patArr[patIdxEnd]) != '*' && strIdxStart <= strIdxEnd) { |
121 | if (ch != '?') { |
122 | if (ch != strArr[strIdxEnd]) { |
123 | return false;// Character mismatch |
124 | } |
125 | } |
126 | patIdxEnd--; |
127 | strIdxEnd--; |
128 | } |
129 | if (strIdxStart > strIdxEnd) { |
130 | // All characters in the string are used. Check if only '*'s are |
131 | // left in the pattern. If so, we succeeded. Otherwise failure. |
132 | for (int i = patIdxStart; i <= patIdxEnd; i++) { |
133 | if (patArr[i] != '*') { |
134 | return false; |
135 | } |
136 | } |
137 | return true; |
138 | } |
139 | |
140 | // process pattern between stars. padIdxStart and patIdxEnd point |
141 | // always to a '*'. |
142 | while (patIdxStart != patIdxEnd && strIdxStart <= strIdxEnd) { |
143 | int patIdxTmp = -1; |
144 | for (int i = patIdxStart + 1; i <= patIdxEnd; i++) { |
145 | if (patArr[i] == '*') { |
146 | patIdxTmp = i; |
147 | break; |
148 | } |
149 | } |
150 | if (patIdxTmp == patIdxStart + 1) { |
151 | // Two stars next to each other, skip the first one. |
152 | patIdxStart++; |
153 | continue; |
154 | } |
155 | // Find the pattern between padIdxStart & padIdxTmp in str between |
156 | // strIdxStart & strIdxEnd |
157 | int patLength = (patIdxTmp - patIdxStart - 1); |
158 | int strLength = (strIdxEnd - strIdxStart + 1); |
159 | int foundIdx = -1; |
160 | strLoop: for (int i = 0; i <= strLength - patLength; i++) { |
161 | for (int j = 0; j < patLength; j++) { |
162 | ch = patArr[patIdxStart + j + 1]; |
163 | if (ch != '?') { |
164 | if (ch != strArr[strIdxStart + i + j]) { |
165 | continue strLoop; |
166 | } |
167 | } |
168 | } |
169 | |
170 | foundIdx = strIdxStart + i; |
171 | break; |
172 | } |
173 | |
174 | if (foundIdx == -1) { |
175 | return false; |
176 | } |
177 | |
178 | patIdxStart = patIdxTmp; |
179 | strIdxStart = foundIdx + patLength; |
180 | } |
181 | |
182 | // All characters in the string are used. Check if only '*'s are left |
183 | // in the pattern. If so, we succeeded. Otherwise failure. |
184 | for (int i = patIdxStart; i <= patIdxEnd; i++) { |
185 | if (patArr[i] != '*') { |
186 | return false; |
187 | } |
188 | } |
189 | |
190 | return true; |
191 | } |
192 | |
193 | /** |
194 | * <p> |
195 | * This method takes a String key and a map from Strings to values of any |
196 | * type. During processing, the method will identify the most specific key |
197 | * in the map that matches the line. Once the correct is identified, its |
198 | * value is returned. Note that if the map contains the wildcard string "*" |
199 | * as a key, then it will serve as the "default" case, matching every line |
200 | * that does not match anything else. |
201 | * |
202 | * <p> |
203 | * If no matching prefix is found, a {@link IllegalStateException} will be |
204 | * thrown. |
205 | * |
206 | * <p> |
207 | * Null keys are not allowed in the map. |
208 | * |
209 | * @param line An input string |
210 | * @return the value whose prefix matches the given line |
211 | */ |
212 | public S match(String line) { |
213 | |
214 | S value = null; |
215 | Assert.notNull(line, "A non-null key must be provided to match against."); |
216 | |
217 | for (String key : sorted) { |
218 | if (PatternMatcher.match(key, line)) { |
219 | value = map.get(key); |
220 | break; |
221 | } |
222 | } |
223 | |
224 | if (value == null) { |
225 | throw new IllegalStateException("Could not find a matching pattern for key=[" + line + "]"); |
226 | } |
227 | return value; |
228 | |
229 | } |
230 | |
231 | } |