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.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   * クラスパスから {@link Action} を検索し、クラス名やメソッド名、そのクラスやメソッドに指定された
56   * {@link org.seasar.cubby.action.Path}
57   * の情報からアクションのパスを抽出し、リクエストされたパスをどのメソッドに振り分けるかを決定します。
58   * 
59   * @author baba
60   * @since 1.0.0
61   */
62  public class PathResolverImpl implements PathResolver, Disposable {
63  
64  	/** ロガー */
65  	private static final Logger logger = Logger
66  			.getLogger(PathResolverImpl.class);
67  
68  	/** デフォルトの URI エンコーディング */
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  	/** デフォルトの URI パラメータ正規表現 */
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  	/** URI のエンコーディング */
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 	 * @return ルーティング情報
109 	 */
110 	public List<Routing> getRoutings() {
111 		initialize();
112 		return Collections.unmodifiableList(new ArrayList<Routing>(routings
113 				.values()));
114 	}
115 
116 	/**
117 	 * URI エンコーディングを設定します。
118 	 * 
119 	 * @param uriEncoding
120 	 *            URI エンコーディング
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 	 * {@inheritDoc}
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 	 * <p>
158 	 * クラスパスを検索して自動登録されるルーティング情報以外にも、このメソッドによって手動でルーティング情報を登録できます。
159 	 * </p>
160 	 * 
161 	 * @param actionPath
162 	 *            アクションのパス
163 	 * @param actionClass
164 	 *            アクションクラス
165 	 * @param methodName
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 	 * <p>
176 	 * クラスパスを検索して自動登録されるルーティング情報以外にも、このメソッドによって手動でルーティング情報を登録できます。
177 	 * </p>
178 	 * 
179 	 * @param actionPath
180 	 *            アクションのパス
181 	 * @param actionClass
182 	 *            アクションクラス
183 	 * @param methodName
184 	 *            アクションメソッド名
185 	 * @param requestMethods
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 	 * @param actionPath
210 	 *            アクションのパス
211 	 * @param actionClass
212 	 *            アクションクラス
213 	 * @param method
214 	 *            アクションメソッド
215 	 * @param requestMethods
216 	 *            リクエストメソッド
217 	 * @param auto
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 	 * {@inheritDoc}
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 	 * @param path
291 	 *            リクエストのパス
292 	 * @param requestMethod
293 	 *            リクエストのメソッド
294 	 * @return 内部フォワード情報、対応する内部フォワード情報が登録されていない場合は <code>null</code>
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 	 * {@inheritDoc}
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 	 * @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>URI 埋め込みパラメータが少ない順</li>
397 		 * <li>正規表現の順(@link {@link String#compareTo(String)})</li>
398 		 * </ul>
399 		 * </p>
400 		 * 
401 		 * @param routing1
402 		 *            比較対象1
403 		 * @param routing2
404 		 *            比較対象2
405 		 * @return 比較結果
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 	 * @author baba
445 	 */
446 	class ActionClassCollector extends ClassCollector {
447 
448 		/**
449 		 * デフォルトコンストラクタ。
450 		 */
451 		public ActionClassCollector() {
452 			super(namingConvention);
453 		}
454 
455 		/**
456 		 * 指定されたパッケージとクラス名からクラスを検索し、アクションクラスであれば{@link PathResolverImpl}に登録します。
457 		 * 
458 		 * @param packageName
459 		 *            パッケージ名
460 		 * @param shortClassName
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 	 * @param <T>
503 	 *            型
504 	 * @param className
505 	 *            クラス名
506 	 * @return クラス
507 	 */
508 	@SuppressWarnings("unchecked")
509 	private static <T> Class<T> classForName(final String className) {
510 		return ClassUtil.forName(className);
511 	}
512 
513 	/**
514 	 * {@inheritDoc}
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 	 * @param actionClass
574 	 *            クラス
575 	 * @param methodName
576 	 *            メソッド
577 	 * @return ルーティング情報
578 	 * @throws ActionRuntimeException
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 }