1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.seasar.cubby.routing.impl;
17
18 import static org.seasar.cubby.CubbyConstants.INTERNAL_FORWARD_DIRECTORY;
19
20 import java.io.IOException;
21 import java.io.UnsupportedEncodingException;
22 import java.lang.reflect.Method;
23 import java.net.URLDecoder;
24 import java.net.URLEncoder;
25 import java.util.ArrayList;
26 import java.util.Collections;
27 import java.util.Comparator;
28 import java.util.HashMap;
29 import java.util.Iterator;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.TreeMap;
33 import java.util.Map.Entry;
34 import java.util.regex.Matcher;
35 import java.util.regex.Pattern;
36
37 import org.seasar.cubby.action.Action;
38 import org.seasar.cubby.action.RequestMethod;
39 import org.seasar.cubby.exception.ActionRuntimeException;
40 import org.seasar.cubby.exception.DuplicateRoutingRuntimeException;
41 import org.seasar.cubby.routing.InternalForwardInfo;
42 import org.seasar.cubby.routing.PathResolver;
43 import org.seasar.cubby.routing.Routing;
44 import org.seasar.cubby.util.CubbyUtils;
45 import org.seasar.cubby.util.QueryStringBuilder;
46 import org.seasar.framework.convention.NamingConvention;
47 import org.seasar.framework.exception.IORuntimeException;
48 import org.seasar.framework.log.Logger;
49 import org.seasar.framework.util.ClassUtil;
50 import org.seasar.framework.util.Disposable;
51 import org.seasar.framework.util.DisposableUtil;
52 import org.seasar.framework.util.StringUtil;
53
54
55
56
57
58
59
60
61
62 public class PathResolverImpl implements PathResolver, Disposable {
63
64
65 private static final Logger logger = Logger
66 .getLogger(PathResolverImpl.class);
67
68
69 private static final String DEFAULT_URI_ENCODING = "UTF-8";
70
71
72 private static Pattern URI_PARAMETER_MATCHING_PATTERN = Pattern
73 .compile("([{]([^}]+)[}])([^{]*)");
74
75
76 private static final String DEFAULT_URI_PARAMETER_REGEX = "[a-zA-Z0-9]+";
77
78
79 private boolean initialized;
80
81
82 private NamingConvention namingConvention;
83
84
85 private final Comparator<Routing> routingComparator = new RoutingComparator();
86
87
88 private final Map<Routing, Routing> routings = new TreeMap<Routing, Routing>(
89 routingComparator);
90
91
92 private String uriEncoding = DEFAULT_URI_ENCODING;
93
94
95
96
97 private int priorityCounter = 0;
98
99
100
101
102 public PathResolverImpl() {
103 }
104
105
106
107
108
109
110 public List<Routing> getRoutings() {
111 initialize();
112 return Collections.unmodifiableList(new ArrayList<Routing>(routings
113 .values()));
114 }
115
116
117
118
119
120
121
122 public void setUriEncoding(final String uriEncoding) {
123 this.uriEncoding = uriEncoding;
124 }
125
126
127
128
129 public void initialize() {
130 if (!initialized) {
131 final ClassCollector classCollector = new ActionClassCollector();
132 classCollector.collect();
133
134 DisposableUtil.add(this);
135 initialized = true;
136 }
137 }
138
139
140
141
142 public void dispose() {
143 final List<Routing> removes = new ArrayList<Routing>();
144 for (final Routing routing : routings.keySet()) {
145 if (routing.isAuto()) {
146 removes.add(routing);
147 }
148 }
149 for (final Routing routing : removes) {
150 routings.remove(routing);
151 }
152 initialized = false;
153 }
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168 public void add(final String actionPath,
169 final Class<? extends Action> actionClass, final String methodName) {
170 this.add(actionPath, actionClass, methodName, new RequestMethod[0]);
171 }
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188 public void add(final String actionPath,
189 final Class<? extends Action> actionClass, final String methodName,
190 final RequestMethod... requestMethods) {
191
192 final Method method = ClassUtil.getMethod(actionClass, methodName,
193 new Class<?>[0]);
194 if (requestMethods == null || requestMethods.length == 0) {
195 for (final RequestMethod requestMethod : CubbyUtils.DEFAULT_ACCEPT_ANNOTATION
196 .value()) {
197 this.add(actionPath, actionClass, method, requestMethod, false);
198 }
199 } else {
200 for (final RequestMethod requestMethod : requestMethods) {
201 this.add(actionPath, actionClass, method, requestMethod, false);
202 }
203 }
204 }
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220 private void add(final String actionPath,
221 final Class<? extends Action> actionClass, final Method method,
222 final RequestMethod requestMethod, final boolean auto) {
223
224 final Matcher matcher = URI_PARAMETER_MATCHING_PATTERN
225 .matcher(actionPath);
226 String uriRegex = actionPath;
227 final List<String> uriParameterNames = new ArrayList<String>();
228 while (matcher.find()) {
229 final String holder = matcher.group(2);
230 final String[] tokens = CubbyUtils.split2(holder, ',');
231 uriParameterNames.add(tokens[0]);
232 final String uriParameterRegex;
233 if (tokens.length == 1) {
234 uriParameterRegex = DEFAULT_URI_PARAMETER_REGEX;
235 } else {
236 uriParameterRegex = tokens[1];
237 }
238 uriRegex = StringUtil.replace(uriRegex, matcher.group(1),
239 regexGroup(uriParameterRegex));
240 }
241 uriRegex = "^" + uriRegex + "$";
242 final Pattern pattern = Pattern.compile(uriRegex);
243
244 final String onSubmit = CubbyUtils.getOnSubmit(method);
245
246 final int priority = auto ? CubbyUtils.getPriority(method)
247 : priorityCounter++;
248
249 final Routing routing = new RoutingImpl(actionClass, method,
250 actionPath, uriParameterNames, pattern, requestMethod,
251 onSubmit, priority, auto);
252
253 if (routings.containsKey(routing)) {
254 final Routing duplication = routings.get(routing);
255 if (!routing.getActionClass().equals(duplication.getActionClass())
256 || !routing.getMethod().equals(duplication.getMethod())) {
257 throw new DuplicateRoutingRuntimeException("ECUB0001",
258 new Object[] { routing, duplication });
259 }
260 } else {
261 routings.put(routing, routing);
262 if (logger.isDebugEnabled()) {
263 logger.log("DCUB0007", new Object[] { routing });
264 }
265 }
266 }
267
268
269
270
271 public InternalForwardInfo getInternalForwardInfo(final String path,
272 final String requestMethod) {
273 initialize();
274
275 final String decodedPath;
276 try {
277 decodedPath = URLDecoder.decode(path, uriEncoding);
278 } catch (final IOException e) {
279 throw new IORuntimeException(e);
280 }
281
282 final InternalForwardInfo internalForwardInfo = findInternalForwardInfo(
283 decodedPath, requestMethod);
284 return internalForwardInfo;
285 }
286
287
288
289
290
291
292
293
294
295
296 private InternalForwardInfo findInternalForwardInfo(final String path,
297 final String requestMethod) {
298 final Iterator<Routing> iterator = routings.values().iterator();
299 while (iterator.hasNext()) {
300 final Routing routing = iterator.next();
301 final Matcher matcher = routing.getPattern().matcher(path);
302 if (matcher.find()) {
303 if (routing.isAcceptable(requestMethod)) {
304 final Map<String, Routing> onSubmitRoutings = new HashMap<String, Routing>();
305 onSubmitRoutings.put(routing.getOnSubmit(), routing);
306 while (iterator.hasNext()) {
307 final Routing anotherRouting = iterator.next();
308 if (routing.getPattern().pattern().equals(
309 anotherRouting.getPattern().pattern())
310 && routing.getRequestMethod().equals(
311 anotherRouting.getRequestMethod())) {
312 onSubmitRoutings.put(anotherRouting.getOnSubmit(),
313 anotherRouting);
314 }
315 }
316
317 final Map<String, String[]> uriParameters = new HashMap<String, String[]>();
318 for (int i = 0; i < matcher.groupCount(); i++) {
319 final String name = routing.getUriParameterNames().get(
320 i);
321 final String value = matcher.group(i + 1);
322 uriParameters.put(name, new String[] { value });
323 }
324 final String inernalFowardPath = buildInternalForwardPath(uriParameters);
325
326 final InternalForwardInfo internalForwardInfo = new InternalForwardInfoImpl(
327 inernalFowardPath, onSubmitRoutings);
328
329 return internalForwardInfo;
330 }
331 }
332 }
333
334 return null;
335 }
336
337
338
339
340 public String buildInternalForwardPath(
341 final Map<String, String[]> parameters) {
342 final StringBuilder builder = new StringBuilder(100);
343 builder.append(INTERNAL_FORWARD_DIRECTORY);
344 if (parameters != null && !parameters.isEmpty()) {
345 builder.append("?");
346 final QueryStringBuilder query = new QueryStringBuilder();
347 if (!StringUtil.isEmpty(uriEncoding)) {
348 query.setEncode(uriEncoding);
349 }
350 for (final Entry<String, String[]> entry : parameters.entrySet()) {
351 for (final String parameter : entry.getValue()) {
352 query.addParam(entry.getKey(), parameter);
353 }
354 }
355 builder.append(query.toString());
356 }
357 return builder.toString();
358 }
359
360
361
362
363
364
365
366 public void setNamingConvention(final NamingConvention namingConvention) {
367 this.namingConvention = namingConvention;
368 }
369
370
371
372
373
374
375
376
377 private static String regexGroup(final String regex) {
378 return "(" + regex + ")";
379 }
380
381
382
383
384
385
386 static class RoutingComparator implements Comparator<Routing> {
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407 public int compare(final Routing routing1, final Routing routing2) {
408 int compare = routing1.getPriority() - routing2.getPriority();
409 if (compare != 0) {
410 return compare;
411 }
412 compare = routing1.getUriParameterNames().size()
413 - routing2.getUriParameterNames().size();
414 if (compare != 0) {
415 return compare;
416 }
417 compare = routing1.getPattern().pattern().compareTo(
418 routing2.getPattern().pattern());
419 if (compare != 0) {
420 return compare;
421 }
422 compare = routing1.getRequestMethod().compareTo(
423 routing2.getRequestMethod());
424 if (compare != 0) {
425 return compare;
426 }
427 if (routing1.getOnSubmit() == routing2.getOnSubmit()) {
428 compare = 0;
429 } else if (routing1.getOnSubmit() == null) {
430 compare = -1;
431 } else if (routing2.getOnSubmit() == null) {
432 compare = 1;
433 } else {
434 compare = routing1.getOnSubmit().compareTo(
435 routing2.getOnSubmit());
436 }
437 return compare;
438 }
439 }
440
441
442
443
444
445
446 class ActionClassCollector extends ClassCollector {
447
448
449
450
451 public ActionClassCollector() {
452 super(namingConvention);
453 }
454
455
456
457
458
459
460
461
462
463 public void processClass(final String packageName,
464 final String shortClassName) {
465 if (shortClassName.indexOf('$') != -1) {
466 return;
467 }
468 final String className = ClassUtil.concatName(packageName,
469 shortClassName);
470 if (!namingConvention.isTargetClassName(className)) {
471 return;
472 }
473 if (!className.endsWith(namingConvention.getActionSuffix())) {
474 return;
475 }
476 final Class<? extends Action> clazz = classForName(className);
477 if (!CubbyUtils.isActionClass(clazz)) {
478 return;
479 }
480 if (namingConvention.isSkipClass(clazz)) {
481 return;
482 }
483
484 for (final Method method : clazz.getMethods()) {
485 if (CubbyUtils.isActionMethod(method)) {
486 final String actionPath = CubbyUtils.getActionPath(clazz,
487 method);
488 final RequestMethod[] acceptableRequestMethods = CubbyUtils
489 .getAcceptableRequestMethods(clazz, method);
490 for (final RequestMethod requestMethod : acceptableRequestMethods) {
491 add(actionPath, clazz, method, requestMethod, true);
492 }
493 }
494 }
495 }
496
497 }
498
499
500
501
502
503
504
505
506
507
508 @SuppressWarnings("unchecked")
509 private static <T> Class<T> classForName(final String className) {
510 return ClassUtil.forName(className);
511 }
512
513
514
515
516 public String reverseLookup(final Class<? extends Action> actionClass,
517 final String methodName, final Map<String, String[]> parameters) {
518 final Routing routing = findRouting(actionClass, methodName);
519 final String actionPath = routing.getActionPath();
520
521 final Matcher matcher = URI_PARAMETER_MATCHING_PATTERN
522 .matcher(actionPath);
523 final Map<String, String[]> copyOfParameters = new HashMap<String, String[]>(
524 parameters);
525 String redirectPath = actionPath;
526 while (matcher.find()) {
527 final String holder = matcher.group(2);
528 final String[] tokens = CubbyUtils.split2(holder, ',');
529 final String uriParameterName = tokens[0];
530 if (!copyOfParameters.containsKey(uriParameterName)) {
531 throw new ActionRuntimeException("ECUB0104", new Object[] {
532 actionPath, uriParameterName });
533 }
534 final String value = copyOfParameters.remove(uriParameterName)[0];
535 final String uriParameterRegex;
536 if (tokens.length == 1) {
537 uriParameterRegex = DEFAULT_URI_PARAMETER_REGEX;
538 } else {
539 uriParameterRegex = tokens[1];
540 }
541 if (!value.matches(uriParameterRegex)) {
542 throw new ActionRuntimeException("ECUB0105",
543 new Object[] { actionPath, uriParameterName, value,
544 uriParameterRegex });
545 }
546 try {
547 final String encodedValue = URLEncoder.encode(value,
548 uriEncoding);
549 redirectPath = StringUtil.replace(redirectPath, matcher
550 .group(1), encodedValue);
551 } catch (UnsupportedEncodingException e) {
552 throw new IORuntimeException(e);
553 }
554 }
555 if (!copyOfParameters.isEmpty()) {
556 final QueryStringBuilder builder = new QueryStringBuilder();
557 builder.setEncode(uriEncoding);
558 for (final Entry<String, String[]> entry : copyOfParameters
559 .entrySet()) {
560 for (final String value : entry.getValue()) {
561 builder.addParam(entry.getKey(), value);
562 }
563 }
564 redirectPath += "?" + builder.toString();
565 }
566
567 return redirectPath;
568 }
569
570
571
572
573
574
575
576
577
578
579
580
581 private Routing findRouting(final Class<? extends Action> actionClass,
582 final String methodName) {
583 for (final Routing routing : routings.values()) {
584 if (actionClass.getCanonicalName().equals(
585 routing.getActionClass().getCanonicalName())) {
586 if (methodName.equals(routing.getMethod().getName())) {
587 return routing;
588 }
589 }
590 }
591 throw new ActionRuntimeException("ECUB0103", new Object[] {
592 actionClass, methodName });
593 }
594
595 }