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