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