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