View Javadoc

1   /*
2    * Copyright 2004-2008 the Seasar Foundation and the Others.
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,
13   * either express or implied. See the License for the specific language
14   * governing permissions and limitations under the License.
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.Path;
39  import org.seasar.cubby.action.RequestMethod;
40  import org.seasar.cubby.controller.ClassDetector;
41  import org.seasar.cubby.controller.DetectClassProcessor;
42  import org.seasar.cubby.exception.ActionRuntimeException;
43  import org.seasar.cubby.exception.DuplicateRoutingRuntimeException;
44  import org.seasar.cubby.exception.IllegalRoutingRuntimeException;
45  import org.seasar.cubby.routing.InternalForwardInfo;
46  import org.seasar.cubby.routing.PathResolver;
47  import org.seasar.cubby.routing.PathTemplateParser;
48  import org.seasar.cubby.routing.Routing;
49  import org.seasar.cubby.util.CubbyUtils;
50  import org.seasar.cubby.util.QueryStringBuilder;
51  import org.seasar.framework.convention.NamingConvention;
52  import org.seasar.framework.exception.IORuntimeException;
53  import org.seasar.framework.log.Logger;
54  import org.seasar.framework.util.ClassUtil;
55  import org.seasar.framework.util.Disposable;
56  import org.seasar.framework.util.DisposableUtil;
57  import org.seasar.framework.util.StringUtil;
58  
59  /**
60   * クラスパスから {@link Action} を検索し、クラス名やメソッド名、そのクラスやメソッドに指定された
61   * {@link org.seasar.cubby.action.Path}
62   * の情報からアクションのパスを抽出し、リクエストされたパスをどのメソッドに振り分けるかを決定します。
63   * 
64   * @author baba
65   * @since 1.0.0
66   */
67  public class PathResolverImpl implements PathResolver, DetectClassProcessor,
68  		Disposable {
69  
70  	/** ロガー */
71  	private static final Logger logger = Logger
72  			.getLogger(PathResolverImpl.class);
73  
74  	/** インスタンスが初期化済みであることを示します。 */
75  	private boolean initialized;
76  
77  	/** 命名規約。 */
78  	private NamingConvention namingConvention;
79  
80  	/** ルーティングのコンパレータ。 */
81  	private final Comparator<Routing> routingComparator = new RoutingComparator();
82  
83  	/** 登録されたルーティングのマップ。 */
84  	private final Map<Routing, Routing> routings = new TreeMap<Routing, Routing>(
85  			routingComparator);
86  
87  	/** クラスパスを走査してクラスを検出するクラス。 */
88  	private ClassDetector classDetector;
89  
90  	/** パステンプレートのパーサー。 */
91  	private PathTemplateParser pathTemplateParser;
92  
93  	/** 手動登録用のプライオリティカウンタ。 */
94  	private int priorityCounter = 0;
95  
96  	/**
97  	 * インスタンス化します。
98  	 */
99  	public PathResolverImpl() {
100 	}
101 
102 	/**
103 	 * ルーティング情報を取得します。
104 	 * 
105 	 * @return ルーティング情報
106 	 */
107 	public List<Routing> getRoutings() {
108 		initialize();
109 		return Collections.unmodifiableList(new ArrayList<Routing>(routings
110 				.values()));
111 	}
112 
113 	/**
114 	 * クラスパスを走査してクラスを検出するクラスを設定します。
115 	 * 
116 	 * @param classDetector
117 	 *            クラスパスを走査してクラスを検出するクラス
118 	 */
119 	public void setClassDetector(final ClassDetector classDetector) {
120 		this.classDetector = classDetector;
121 	}
122 
123 	/**
124 	 * パステンプレートのパーサーを設定します。
125 	 * 
126 	 * @param pathTemplateParser
127 	 *            パステンプレートのパーサー
128 	 */
129 	public void setPathTemplateParser(
130 			final PathTemplateParser pathTemplateParser) {
131 		this.pathTemplateParser = pathTemplateParser;
132 	}
133 
134 	/**
135 	 * 初期化します。
136 	 */
137 	public void initialize() {
138 		if (initialized) {
139 			return;
140 		}
141 		classDetector.detect();
142 		DisposableUtil.add(this);
143 		initialized = true;
144 	}
145 
146 	/**
147 	 * {@inheritDoc}
148 	 */
149 	public void dispose() {
150 		final List<Routing> removes = new ArrayList<Routing>();
151 		for (final Routing routing : routings.keySet()) {
152 			if (routing.isAuto()) {
153 				removes.add(routing);
154 			}
155 		}
156 		for (final Routing routing : removes) {
157 			routings.remove(routing);
158 		}
159 		initialized = false;
160 	}
161 
162 	/**
163 	 * ルーティング情報を登録します。
164 	 * <p>
165 	 * クラスパスを検索して自動登録されるルーティング情報以外にも、このメソッドによって手動でルーティング情報を登録できます。
166 	 * </p>
167 	 * 
168 	 * @param actionPath
169 	 *            アクションのパス
170 	 * @param actionClass
171 	 *            アクションクラス
172 	 * @param methodName
173 	 *            アクションメソッド名
174 	 */
175 	public void add(final String actionPath,
176 			final Class<? extends Action> actionClass, final String methodName) {
177 		this.add(actionPath, actionClass, methodName, new RequestMethod[0]);
178 	}
179 
180 	/**
181 	 * ルーティング情報を登録します。
182 	 * <p>
183 	 * クラスパスを検索して自動登録されるルーティング情報以外にも、このメソッドによって手動でルーティング情報を登録できます。
184 	 * </p>
185 	 * 
186 	 * @param actionPath
187 	 *            アクションのパス
188 	 * @param actionClass
189 	 *            アクションクラス
190 	 * @param methodName
191 	 *            アクションメソッド名
192 	 * @param requestMethods
193 	 *            リクエストメソッド
194 	 */
195 	public void add(final String actionPath,
196 			final Class<? extends Action> actionClass, final String methodName,
197 			final RequestMethod... requestMethods) {
198 
199 		final Method method = ClassUtil.getMethod(actionClass, methodName,
200 				new Class<?>[0]);
201 		if (requestMethods == null || requestMethods.length == 0) {
202 			for (final RequestMethod requestMethod : CubbyUtils.DEFAULT_ACCEPT_ANNOTATION
203 					.value()) {
204 				this.add(actionPath, actionClass, method, requestMethod, false);
205 			}
206 		} else {
207 			for (final RequestMethod requestMethod : requestMethods) {
208 				this.add(actionPath, actionClass, method, requestMethod, false);
209 			}
210 		}
211 	}
212 
213 	/**
214 	 * ルーティング情報を登録します。
215 	 * 
216 	 * @param actionPath
217 	 *            アクションのパス
218 	 * @param actionClass
219 	 *            アクションクラス
220 	 * @param method
221 	 *            アクションメソッド
222 	 * @param requestMethods
223 	 *            リクエストメソッド
224 	 * @param auto
225 	 *            自動登録かどうか
226 	 */
227 	private void add(final String actionPath,
228 			final Class<? extends Action> actionClass, final Method method,
229 			final RequestMethod requestMethod, final boolean auto) {
230 
231 		if (!CubbyUtils.isActionClass(actionClass)) {
232 			throw new IllegalRoutingRuntimeException("ECUB0002",
233 					new Object[] { actionClass });
234 		} else if (!CubbyUtils.isActionMethod(method)) {
235 			throw new IllegalRoutingRuntimeException("ECUB0003",
236 					new Object[] { method });
237 		}
238 
239 		final List<String> uriParameterNames = new ArrayList<String>();
240 		final String uriRegex = pathTemplateParser.parse(actionPath,
241 				new PathTemplateParser.Handler() {
242 
243 					public String handle(final String name, final String regex) {
244 						uriParameterNames.add(name);
245 						return regexGroup(regex);
246 					}
247 
248 				});
249 		final Pattern pattern = Pattern.compile("^" + uriRegex + "$");
250 
251 		final String onSubmit = CubbyUtils.getOnSubmit(method);
252 
253 		final int priority = auto ? CubbyUtils.getPriority(method)
254 				: priorityCounter++;
255 
256 		final Routing routing = new RoutingImpl(actionClass, method,
257 				actionPath, uriParameterNames, pattern, requestMethod,
258 				onSubmit, priority, auto);
259 
260 		if (routings.containsKey(routing)) {
261 			final Routing duplication = routings.get(routing);
262 			if (!routing.getActionClass().equals(duplication.getActionClass())
263 					|| !routing.getMethod().equals(duplication.getMethod())) {
264 				throw new DuplicateRoutingRuntimeException("ECUB0001",
265 						new Object[] { routing, duplication });
266 			}
267 		} else {
268 			routings.put(routing, routing);
269 			if (logger.isDebugEnabled()) {
270 				logger.log("DCUB0007", new Object[] { routing });
271 			}
272 		}
273 	}
274 
275 	/**
276 	 * {@inheritDoc}
277 	 */
278 	public InternalForwardInfo getInternalForwardInfo(final String path,
279 			final String requestMethod, final String characterEncoding) {
280 		initialize();
281 
282 		final InternalForwardInfo internalForwardInfo = findInternalForwardInfo(
283 				decode(path, characterEncoding), requestMethod,
284 				characterEncoding);
285 		return internalForwardInfo;
286 	}
287 
288 	/**
289 	 * 指定されたパス、メソッドに対応する内部フォワード情報を検索します。
290 	 * 
291 	 * @param path
292 	 *            リクエストのパス
293 	 * @param requestMethod
294 	 *            リクエストのメソッド
295 	 * @return 内部フォワード情報、対応する内部フォワード情報が登録されていない場合は <code>null</code>
296 	 */
297 	private InternalForwardInfo findInternalForwardInfo(final String path,
298 			final String requestMethod, final String characterEncoding) {
299 		final Iterator<Routing> iterator = routings.values().iterator();
300 		while (iterator.hasNext()) {
301 			final Routing routing = iterator.next();
302 			final Matcher matcher = routing.getPattern().matcher(path);
303 			if (matcher.find()) {
304 				if (routing.isAcceptable(requestMethod)) {
305 					final Map<String, Routing> onSubmitRoutings = new HashMap<String, Routing>();
306 					onSubmitRoutings.put(routing.getOnSubmit(), routing);
307 					while (iterator.hasNext()) {
308 						final Routing anotherRouting = iterator.next();
309 						if (routing.getPattern().pattern().equals(
310 								anotherRouting.getPattern().pattern())
311 								&& routing.getRequestMethod().equals(
312 										anotherRouting.getRequestMethod())) {
313 							onSubmitRoutings.put(anotherRouting.getOnSubmit(),
314 									anotherRouting);
315 						}
316 					}
317 
318 					final Map<String, String[]> uriParameters = new HashMap<String, String[]>();
319 					for (int i = 0; i < matcher.groupCount(); i++) {
320 						final String name = routing.getUriParameterNames().get(
321 								i);
322 						final String value = matcher.group(i + 1);
323 						uriParameters.put(name, new String[] { value });
324 					}
325 					final String inernalFowardPath = buildInternalForwardPath(
326 							uriParameters, characterEncoding);
327 
328 					final InternalForwardInfo internalForwardInfo = new InternalForwardInfoImpl(
329 							inernalFowardPath, onSubmitRoutings);
330 
331 					return internalForwardInfo;
332 				}
333 			}
334 		}
335 
336 		return null;
337 	}
338 
339 	/**
340 	 * {@inheritDoc}
341 	 */
342 	public String buildInternalForwardPath(
343 			final Map<String, String[]> parameters,
344 			final String characterEncoding) {
345 		final StringBuilder path = new StringBuilder(100);
346 		path.append(INTERNAL_FORWARD_DIRECTORY);
347 		if (parameters != null && !parameters.isEmpty()) {
348 			path.append("?");
349 			final QueryStringBuilder query = new QueryStringBuilder();
350 			if (!StringUtil.isEmpty(characterEncoding)) {
351 				query.setEncode(characterEncoding);
352 			}
353 			for (final Entry<String, String[]> entry : parameters.entrySet()) {
354 				for (final String parameter : entry.getValue()) {
355 					query.addParam(entry.getKey(), parameter);
356 				}
357 			}
358 			path.append(query.toString());
359 		}
360 		return path.toString();
361 	}
362 
363 	/**
364 	 * 命名規約を設定します。
365 	 * 
366 	 * @param namingConvention
367 	 *            命名規約
368 	 */
369 	public void setNamingConvention(final NamingConvention namingConvention) {
370 		this.namingConvention = namingConvention;
371 	}
372 
373 	/**
374 	 * 指定された正規表現を括弧「()」で囲んで正規表現のグループにします。
375 	 * 
376 	 * @param regex
377 	 *            正規表現
378 	 * @return 正規表現のグループ
379 	 */
380 	private static String regexGroup(final String regex) {
381 		return "(" + regex + ")";
382 	}
383 
384 	/**
385 	 * ルーティングのコンパレータ。
386 	 * 
387 	 * @author baba
388 	 */
389 	static class RoutingComparator implements Comparator<Routing> {
390 
391 		/**
392 		 * routing1 と routing2 を比較します。
393 		 * <p>
394 		 * 正規表現パターンと HTTP メソッドが同じ場合は同値とみなします。
395 		 * </p>
396 		 * <p>
397 		 * また、大小関係は以下のようになります。
398 		 * <ul>
399 		 * <li>優先度(@link {@link Path#priority()})が小さい順</li>
400 		 * <li>URI 埋め込みパラメータが少ない順</li>
401 		 * <li>正規表現の順(@link {@link String#compareTo(String)})</li>
402 		 * </ul>
403 		 * </p>
404 		 * 
405 		 * @param routing1
406 		 *            比較対象1
407 		 * @param routing2
408 		 *            比較対象2
409 		 * @return 比較結果
410 		 */
411 		public int compare(final Routing routing1, final Routing routing2) {
412 			int compare = routing1.getPriority() - routing2.getPriority();
413 			if (compare != 0) {
414 				return compare;
415 			}
416 			compare = routing1.getUriParameterNames().size()
417 					- routing2.getUriParameterNames().size();
418 			if (compare != 0) {
419 				return compare;
420 			}
421 			compare = routing1.getPattern().pattern().compareTo(
422 					routing2.getPattern().pattern());
423 			if (compare != 0) {
424 				return compare;
425 			}
426 			compare = routing1.getRequestMethod().compareTo(
427 					routing2.getRequestMethod());
428 			if (compare != 0) {
429 				return compare;
430 			}
431 			if (routing1.getOnSubmit() == routing2.getOnSubmit()) {
432 				compare = 0;
433 			} else if (routing1.getOnSubmit() == null) {
434 				compare = -1;
435 			} else if (routing2.getOnSubmit() == null) {
436 				compare = 1;
437 			} else {
438 				compare = routing1.getOnSubmit().compareTo(
439 						routing2.getOnSubmit());
440 			}
441 			return compare;
442 		}
443 	}
444 
445 	/**
446 	 * {@inheritDoc}
447 	 */
448 	public String reverseLookup(final Class<? extends Action> actionClass,
449 			final String methodName, final Map<String, String[]> parameters,
450 			final String characterEncoding) {
451 		final Routing routing = findRouting(actionClass, methodName);
452 		final String actionPath = routing.getActionPath();
453 		final Map<String, String[]> copyOfParameters = new HashMap<String, String[]>(
454 				parameters);
455 		final StringBuilder path = new StringBuilder(100);
456 		path.append(pathTemplateParser.parse(actionPath,
457 				new PathTemplateParser.Handler() {
458 
459 					public String handle(final String name, final String regex) {
460 						if (!copyOfParameters.containsKey(name)) {
461 							throw new ActionRuntimeException("ECUB0104",
462 									new Object[] { actionPath, name });
463 						}
464 						final String value = copyOfParameters.remove(name)[0];
465 						if (!value.matches(regex)) {
466 							throw new ActionRuntimeException("ECUB0105",
467 									new Object[] { actionPath, name, value,
468 											regex });
469 						}
470 						return encode(value, characterEncoding);
471 					}
472 
473 				}));
474 
475 		if (!copyOfParameters.isEmpty()) {
476 			final QueryStringBuilder builder = new QueryStringBuilder();
477 			if (characterEncoding != null) {
478 				builder.setEncode(characterEncoding);
479 			}
480 			for (final Entry<String, String[]> entry : copyOfParameters
481 					.entrySet()) {
482 				for (final String value : entry.getValue()) {
483 					builder.addParam(entry.getKey(), value);
484 				}
485 			}
486 			path.append('?');
487 			path.append(builder.toString());
488 		}
489 
490 		return path.toString();
491 	}
492 
493 	/**
494 	 * 指定されたクラス、メソッドに対応するルーティング情報を検索します。
495 	 * 
496 	 * @param actionClass
497 	 *            クラス
498 	 * @param methodName
499 	 *            メソッド
500 	 * @return ルーティング情報
501 	 * @throws ActionRuntimeException
502 	 *             ルーティング情報が見つからなかった場合
503 	 */
504 	private Routing findRouting(final Class<? extends Action> actionClass,
505 			final String methodName) {
506 		for (final Routing routing : routings.values()) {
507 			if (actionClass.getCanonicalName().equals(
508 					routing.getActionClass().getCanonicalName())) {
509 				if (methodName.equals(routing.getMethod().getName())) {
510 					return routing;
511 				}
512 			}
513 		}
514 		throw new ActionRuntimeException("ECUB0103", new Object[] {
515 				actionClass, methodName });
516 	}
517 
518 	/**
519 	 * {@inheritDoc}
520 	 * <p>
521 	 * 指定されたパッケージ名、クラス名から導出されるクラスがアクションクラスだった場合はルーティングを登録します。
522 	 * </p>
523 	 */
524 	public void processClass(final String packageName,
525 			final String shortClassName) {
526 		if (shortClassName.indexOf('$') != -1) {
527 			return;
528 		}
529 		final String className = ClassUtil.concatName(packageName,
530 				shortClassName);
531 		if (!namingConvention.isTargetClassName(className)) {
532 			return;
533 		}
534 		if (!className.endsWith(namingConvention.getActionSuffix())) {
535 			return;
536 		}
537 		final Class<?> clazz = ClassUtil.forName(className);
538 		if (namingConvention.isSkipClass(clazz)) {
539 			return;
540 		}
541 		if (!CubbyUtils.isActionClass(clazz)) {
542 			return;
543 		}
544 		final Class<? extends Action> actionClass = cast(clazz);
545 
546 		for (final Method method : clazz.getMethods()) {
547 			if (CubbyUtils.isActionMethod(method)) {
548 				final String actionPath = CubbyUtils.getActionPath(actionClass,
549 						method);
550 				final RequestMethod[] acceptableRequestMethods = CubbyUtils
551 						.getAcceptableRequestMethods(clazz, method);
552 				for (final RequestMethod requestMethod : acceptableRequestMethods) {
553 					add(actionPath, actionClass, method, requestMethod, true);
554 				}
555 			}
556 		}
557 	}
558 
559 	/**
560 	 * 指定されたクラスを <code>Class&lt;? extends Action&gt;</code> にキャストします。
561 	 * 
562 	 * @param clazz
563 	 *            クラス
564 	 * @return キャストされたクラス
565 	 */
566 	@SuppressWarnings("unchecked")
567 	private static Class<? extends Action> cast(final Class<?> clazz) {
568 		return Class.class.cast(clazz);
569 	}
570 
571 	/**
572 	 * 指定された文字列を URL エンコードします。
573 	 * 
574 	 * @param str
575 	 *            文字列
576 	 * @param characterEncoding
577 	 *            エンコーディング
578 	 * @return エンコードされた文字列
579 	 */
580 	private static String encode(final String str,
581 			final String characterEncoding) {
582 		if (characterEncoding == null) {
583 			return str;
584 		}
585 		try {
586 			return URLEncoder.encode(str, characterEncoding);
587 		} catch (final UnsupportedEncodingException e) {
588 			throw new IORuntimeException(e);
589 		}
590 	}
591 
592 	/**
593 	 * 指定された URL 文字列をデコードします。
594 	 * 
595 	 * @param str
596 	 *            文字列
597 	 * @param characterEncoding
598 	 *            エンコーディング
599 	 * @return デコードされた文字列
600 	 */
601 	private static String decode(final String str,
602 			final String characterEncoding) {
603 		if (characterEncoding == null) {
604 			return str;
605 		}
606 		try {
607 			return URLDecoder.decode(str, characterEncoding);
608 		} catch (final IOException e) {
609 			throw new IORuntimeException(e);
610 		}
611 	}
612 
613 }