1 /*
2 * Title: PathMapper
3 * Description:
4 *
5 * This software is published under the terms of the OpenSymphony Software
6 * License version 1.1, of which a copy has been included with this
7 * distribution in the LICENSE.txt file.
8 */
9
10 package com.opensymphony.module.sitemesh.mapper;
11
12 import java.util.HashMap;
13 import java.util.Map;
14 import java.util.Iterator;
15
16 /**
17 * The PathMapper is used to map file patterns to keys, and find an approriate
18 * key for a given file path. The pattern rules are consistent with those defined
19 * in the Servlet 2.3 API on the whole. Wildcard patterns are also supported, using
20 * any combination of * and ?.
21 *
22 * <h3>Example</h3>
23 *
24 * <blockquote><code>
25 * PathMapper pm = new PathMapper();<br>
26 * <br>
27 * pm.put("one","/");<br>
28 * pm.put("two","/mydir/*");<br>
29 * pm.put("three","*.xml");<br>
30 * pm.put("four","/myexactfile.html");<br>
31 * pm.put("five","/*\/admin/*.??ml");<br>
32 * <br>
33 * String result1 = pm.get("/mydir/myfile.xml"); // returns "two";<br>
34 * String result2 = pm.get("/mydir/otherdir/admin/myfile.html"); // returns "five";<br>
35 * </code></blockquote>
36 *
37 * @author <a href="mailto:joe@truemesh.com">Joe Walnes</a>
38 * @author <a href="mailto:mcannon@internet.com">Mike Cannon-Brookes</a>
39 * @author <a href="mailto:hani@formicary.net">Hani Suleiman</a>
40 * @version $Revision: 1.2 $
41 */
42 public final class PathMapper {
43 private Map mappings = new HashMap();
44
45 /** Add a key and appropriate matching pattern. */
46 public void put(String key, String pattern) {
47 if (key != null) {
48 mappings.put(pattern, key);
49 }
50 }
51
52 /** Retrieve appropriate key by matching patterns with supplied path. */
53 public String get(String path) {
54 if (path == null) path = "/";
55 String mapped = findKey(path, mappings);
56 if (mapped == null) return null;
57 return (String) mappings.get(mapped);
58 }
59
60 /** Find exact key in mappings. */
61 private static String findKey(String path, Map mappings) {
62 String result = findExactKey(path, mappings);
63 if (result == null) result = findComplexKey(path, mappings);
64 if (result == null) result = findDefaultKey(mappings);
65 return result;
66 }
67
68 /** Check if path matches exact pattern ( /blah/blah.jsp ). */
69 private static String findExactKey(String path, Map mappings) {
70 if (mappings.containsKey(path)) return path;
71 return null;
72 }
73
74 private static String findComplexKey(String path, Map mappings) {
75 Iterator i = mappings.keySet().iterator();
76 String result = null, key = null;
77 while (i.hasNext()) {
78 key = (String) i.next();
79 if (key.length() > 1 && (key.indexOf('?') != -1 || key.indexOf('*') != -1) && match(key, path, false)) {
80 if (result == null || key.length() > result.length()) {
81 // longest key wins
82 result = key;
83 }
84 }
85 }
86 return result;
87 }
88
89 /** Look for root pattern ( / ). */
90 private static String findDefaultKey(Map mappings) {
91 String[] defaultKeys = {"/", "*", "/*"};
92 for (int i = 0; i < defaultKeys.length; i++) {
93 if (mappings.containsKey(defaultKeys[i])) return defaultKeys[i];
94 }
95 return null;
96 }
97
98 private static boolean match(String pattern, String str, boolean isCaseSensitive) {
99 char[] patArr = pattern.toCharArray();
100 char[] strArr = str.toCharArray();
101 int patIdxStart = 0;
102 int patIdxEnd = patArr.length - 1;
103 int strIdxStart = 0;
104 int strIdxEnd = strArr.length - 1;
105 char ch;
106
107 boolean containsStar = false;
108 for (int i = 0; i < patArr.length; i++) {
109 if (patArr[i] == '*') {
110 containsStar = true;
111 break;
112 }
113 }
114
115 if (!containsStar) {
116 // No '*'s, so we make a shortcut
117 if (patIdxEnd != strIdxEnd) {
118 return false; // Pattern and string do not have the same size
119 }
120 for (int i = 0; i <= patIdxEnd; i++) {
121 ch = patArr[i];
122 if (ch != '?') {
123 if (isCaseSensitive && ch != strArr[i]) {
124 return false; // Character mismatch
125 }
126 if (!isCaseSensitive && Character.toUpperCase(ch) !=
127 Character.toUpperCase(strArr[i])) {
128 return false; // Character mismatch
129 }
130 }
131 }
132 return true; // String matches against pattern
133 }
134
135 if (patIdxEnd == 0) {
136 return true; // Pattern contains only '*', which matches anything
137 }
138
139 // Process characters before first star
140 while ((ch = patArr[patIdxStart]) != '*' && strIdxStart <= strIdxEnd) {
141 if (ch != '?') {
142 if (isCaseSensitive && ch != strArr[strIdxStart]) {
143 return false; // Character mismatch
144 }
145 if (!isCaseSensitive && Character.toUpperCase(ch) !=
146 Character.toUpperCase(strArr[strIdxStart])) {
147 return false; // Character mismatch
148 }
149 }
150 patIdxStart++;
151 strIdxStart++;
152 }
153 if (strIdxStart > strIdxEnd) {
154 // All characters in the string are used. Check if only '*'s are
155 // left in the pattern. If so, we succeeded. Otherwise failure.
156 for (int i = patIdxStart; i <= patIdxEnd; i++) {
157 if (patArr[i] != '*') {
158 return false;
159 }
160 }
161 return true;
162 }
163
164 // Process characters after last star
165 while ((ch = patArr[patIdxEnd]) != '*' && strIdxStart <= strIdxEnd) {
166 if (ch != '?') {
167 if (isCaseSensitive && ch != strArr[strIdxEnd]) {
168 return false; // Character mismatch
169 }
170 if (!isCaseSensitive && Character.toUpperCase(ch) !=
171 Character.toUpperCase(strArr[strIdxEnd])) {
172 return false; // Character mismatch
173 }
174 }
175 patIdxEnd--;
176 strIdxEnd--;
177 }
178 if (strIdxStart > strIdxEnd) {
179 // All characters in the string are used. Check if only '*'s are
180 // left in the pattern. If so, we succeeded. Otherwise failure.
181 for (int i = patIdxStart; i <= patIdxEnd; i++) {
182 if (patArr[i] != '*') {
183 return false;
184 }
185 }
186 return true;
187 }
188
189 // process pattern between stars. padIdxStart and patIdxEnd point
190 // always to a '*'.
191 while (patIdxStart != patIdxEnd && strIdxStart <= strIdxEnd) {
192 int patIdxTmp = -1;
193 for (int i = patIdxStart + 1; i <= patIdxEnd; i++) {
194 if (patArr[i] == '*') {
195 patIdxTmp = i;
196 break;
197 }
198 }
199 if (patIdxTmp == patIdxStart + 1) {
200 // Two stars next to each other, skip the first one.
201 patIdxStart++;
202 continue;
203 }
204 // Find the pattern between padIdxStart & padIdxTmp in str between
205 // strIdxStart & strIdxEnd
206 int patLength = (patIdxTmp - patIdxStart - 1);
207 int strLength = (strIdxEnd - strIdxStart + 1);
208 int foundIdx = -1;
209 strLoop:
210 for (int i = 0; i <= strLength - patLength; i++) {
211 for (int j = 0; j < patLength; j++) {
212 ch = patArr[patIdxStart + j + 1];
213 if (ch != '?') {
214 if (isCaseSensitive && ch != strArr[strIdxStart + i + j]) {
215 continue strLoop;
216 }
217 if (!isCaseSensitive && Character.toUpperCase(ch) !=
218 Character.toUpperCase(strArr[strIdxStart + i + j])) {
219 continue strLoop;
220 }
221 }
222 }
223
224 foundIdx = strIdxStart + i;
225 break;
226 }
227
228 if (foundIdx == -1) {
229 return false;
230 }
231
232 patIdxStart = patIdxTmp;
233 strIdxStart = foundIdx + patLength;
234 }
235
236 // All characters in the string are used. Check if only '*'s are left
237 // in the pattern. If so, we succeeded. Otherwise failure.
238 for (int i = patIdxStart; i <= patIdxEnd; i++) {
239 if (patArr[i] != '*') {
240 return false;
241 }
242 }
243 return true;
244 }
245 }