View Javadoc

1   /*
2    * Copyright 2004-2009 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.internal.util.LogMessages.format;
19  
20  import java.io.UnsupportedEncodingException;
21  import java.lang.reflect.Method;
22  import java.util.ArrayList;
23  import java.util.Collection;
24  import java.util.HashMap;
25  import java.util.Iterator;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.TreeMap;
29  import java.util.Map.Entry;
30  import java.util.regex.Matcher;
31  import java.util.regex.Pattern;
32  
33  import org.seasar.cubby.action.Path;
34  import org.seasar.cubby.action.RequestMethod;
35  import org.seasar.cubby.internal.util.MetaUtils;
36  import org.seasar.cubby.internal.util.QueryStringBuilder;
37  import org.seasar.cubby.internal.util.URLBodyEncoder;
38  import org.seasar.cubby.routing.PathInfo;
39  import org.seasar.cubby.routing.PathResolver;
40  import org.seasar.cubby.routing.PathTemplateParser;
41  import org.seasar.cubby.routing.Routing;
42  import org.seasar.cubby.routing.RoutingException;
43  import org.seasar.cubby.util.ActionUtils;
44  import org.slf4j.Logger;
45  import org.slf4j.LoggerFactory;
46  
47  /**
48   * パスに対応するアクションメソッドを解決するためのクラスの実装です。
49   * 
50   * @author baba
51   * @since 1.0.0
52   */
53  public class PathResolverImpl implements PathResolver {
54  
55  	/** ロガー */
56  	private static final Logger logger = LoggerFactory
57  			.getLogger(PathResolverImpl.class);
58  
59  	/** 登録されたルーティングのマップ。 */
60  	private final Map<RoutingKey, Routing> routings = new TreeMap<RoutingKey, Routing>();
61  
62  	/** パステンプレートのパーサー。 */
63  	private PathTemplateParser pathTemplateParser;
64  
65  	/**
66  	 * インスタンス化します。
67  	 * 
68  	 * @param pathTemplateParser
69  	 *            パステンプレートのパーサー
70  	 */
71  	public PathResolverImpl(final PathTemplateParser pathTemplateParser) {
72  		this.pathTemplateParser = pathTemplateParser;
73  	}
74  
75  	/**
76  	 * ルーティング情報を取得します。
77  	 * 
78  	 * @return ルーティング情報
79  	 */
80  	public Collection<Routing> getRoutings() {
81  		return routings.values();
82  	}
83  
84  	/**
85  	 * {@inheritDoc}
86  	 */
87  	public void add(final Class<?> actionClass) {
88  		for (final Method method : actionClass.getMethods()) {
89  			if (ActionUtils.isActionMethod(method)) {
90  				final String actionPath = MetaUtils.getActionPath(actionClass,
91  						method);
92  				final RequestMethod[] acceptableRequestMethods = MetaUtils
93  						.getAcceptableRequestMethods(actionClass, method);
94  				for (final RequestMethod requestMethod : acceptableRequestMethods) {
95  					final String onSubmit = MetaUtils.getOnSubmit(method);
96  					final int priority = MetaUtils.getPriority(method);
97  					this.add(actionPath, actionClass, method, requestMethod,
98  							onSubmit, priority);
99  				}
100 			}
101 		}
102 	}
103 
104 	/**
105 	 * {@inheritDoc}
106 	 */
107 	public void addAll(final Collection<Class<?>> actionClasses) {
108 		for (final Class<?> actionClass : actionClasses) {
109 			add(actionClass);
110 		}
111 	}
112 
113 	/**
114 	 * {@inheritDoc}
115 	 */
116 	public void clear() {
117 		routings.clear();
118 	}
119 
120 	/**
121 	 * {@inheritDoc}
122 	 */
123 	public void add(final String actionPath, final Class<?> actionClass,
124 			final String methodName, final RequestMethod requestMethod,
125 			final String onSubmit, final int priority) {
126 		try {
127 			final Method method = actionClass.getMethod(methodName);
128 			this.add(actionPath, actionClass, method, requestMethod, onSubmit,
129 					priority);
130 		} catch (final NoSuchMethodException e) {
131 			throw new RoutingException(e);
132 		}
133 	}
134 
135 	/**
136 	 * ルーティング情報を登録します。
137 	 * 
138 	 * @param actionPath
139 	 *            アクションのパス
140 	 * @param actionClass
141 	 *            アクションクラス
142 	 * @param method
143 	 *            アクションメソッド
144 	 * @param requestMethods
145 	 *            リクエストメソッド
146 	 * @param onSubmit
147 	 *            アクションメソッドへ振り分けるための要求パラメータ名
148 	 * @param priority
149 	 *            プライオリティ
150 	 */
151 	private void add(final String actionPath, final Class<?> actionClass,
152 			final Method method, final RequestMethod requestMethod,
153 			final String onSubmit, final int priority) {
154 		if (!ActionUtils.isActionMethod(method)) {
155 			throw new RoutingException(format("ECUB0003", method));
156 		}
157 
158 		final List<String> uriParameterNames = new ArrayList<String>();
159 		final String uriRegex = pathTemplateParser.parse(actionPath,
160 				new PathTemplateParser.Handler() {
161 
162 					public String handle(final String name, final String regex) {
163 						uriParameterNames.add(name);
164 						return regexGroup(regex);
165 					}
166 
167 				});
168 		final Pattern pattern = Pattern.compile("^" + uriRegex + "$");
169 
170 		final Routing routing = new RoutingImpl(actionClass, method,
171 				actionPath, uriParameterNames, pattern, requestMethod,
172 				onSubmit, priority);
173 		final RoutingKey key = new RoutingKey(routing);
174 
175 		if (logger.isDebugEnabled()) {
176 			logger.debug(format("DCUB0007", routing));
177 		}
178 		if (routings.containsKey(key)) {
179 			final Routing duplication = routings.get(key);
180 			throw new RoutingException(format("ECUB0001", routing, duplication));
181 		}
182 		routings.put(key, routing);
183 	}
184 
185 	/**
186 	 * {@inheritDoc}
187 	 */
188 	public PathInfo getPathInfo(final String path, final String requestMethod,
189 			final String characterEncoding) {
190 		final Iterator<Routing> iterator = getRoutings().iterator();
191 		while (iterator.hasNext()) {
192 			final Routing routing = iterator.next();
193 			final Matcher matcher = routing.getPattern().matcher(path);
194 			if (matcher.find()) {
195 				if (routing.isAcceptable(requestMethod)) {
196 					final Map<String, Routing> onSubmitRoutings = new HashMap<String, Routing>();
197 					onSubmitRoutings.put(routing.getOnSubmit(), routing);
198 					while (iterator.hasNext()) {
199 						final Routing anotherRouting = iterator.next();
200 						if (routing.getPattern().pattern().equals(
201 								anotherRouting.getPattern().pattern())
202 								&& routing.getRequestMethod().equals(
203 										anotherRouting.getRequestMethod())) {
204 							onSubmitRoutings.put(anotherRouting.getOnSubmit(),
205 									anotherRouting);
206 						}
207 					}
208 
209 					final Map<String, String[]> uriParameters = new HashMap<String, String[]>();
210 					for (int i = 0; i < matcher.groupCount(); i++) {
211 						final String name = routing.getUriParameterNames().get(
212 								i);
213 						final String value = matcher.group(i + 1);
214 						uriParameters.put(name, new String[] { value });
215 					}
216 
217 					final PathInfo pathInfo = new ResolvedPathInfo(
218 							uriParameters, onSubmitRoutings);
219 
220 					return pathInfo;
221 				}
222 			}
223 		}
224 
225 		return null;
226 	}
227 
228 	/**
229 	 * 指定された正規表現を括弧「()」で囲んで正規表現のグループにします。
230 	 * 
231 	 * @param regex
232 	 *            正規表現
233 	 * @return 正規表現のグループ
234 	 */
235 	private static String regexGroup(final String regex) {
236 		return "(" + regex + ")";
237 	}
238 
239 	/**
240 	 * {@inheritDoc}
241 	 */
242 	public String reverseLookup(final Class<?> actionClass,
243 			final String methodName, final Map<String, String[]> parameters,
244 			final String characterEncoding) {
245 		final Collection<Routing> routings = getRoutings();
246 		final Routing routing = findRouting(routings, actionClass, methodName);
247 		final String actionPath = routing.getActionPath();
248 		final Map<String, String[]> copyOfParameters = new HashMap<String, String[]>(
249 				parameters);
250 		final StringBuilder path = new StringBuilder(100);
251 		path.append(pathTemplateParser.parse(actionPath,
252 				new PathTemplateParser.Handler() {
253 
254 					public String handle(final String name, final String regex) {
255 						if (!copyOfParameters.containsKey(name)) {
256 							throw new RoutingException(format("ECUB0104",
257 									actionPath, name));
258 						}
259 						final String value = copyOfParameters.remove(name)[0];
260 						if (!value.matches(regex)) {
261 							throw new RoutingException(format("ECUB0105",
262 									actionPath, name, value, regex));
263 						}
264 						return encode(value, characterEncoding);
265 					}
266 
267 				}));
268 
269 		if (!copyOfParameters.isEmpty()) {
270 			final QueryStringBuilder builder = new QueryStringBuilder();
271 			if (characterEncoding != null) {
272 				builder.setEncode(characterEncoding);
273 			}
274 			for (final Entry<String, String[]> entry : copyOfParameters
275 					.entrySet()) {
276 				for (final String value : entry.getValue()) {
277 					builder.addParam(entry.getKey(), value);
278 				}
279 			}
280 			path.append('?');
281 			path.append(builder.toString());
282 		}
283 
284 		return path.toString();
285 	}
286 
287 	/**
288 	 * 指定されたクラス、メソッドに対応するルーティング情報を検索します。
289 	 * 
290 	 * @param routings
291 	 *            ルーティング情報
292 	 * @param actionClass
293 	 *            クラス
294 	 * @param methodName
295 	 *            メソッド
296 	 * @return ルーティング情報
297 	 * @throws RoutingException
298 	 *             ルーティング情報が見つからなかった場合
299 	 */
300 	private static Routing findRouting(final Collection<Routing> routings,
301 			final Class<?> actionClass, final String methodName) {
302 		for (final Routing routing : routings) {
303 			if (actionClass.getCanonicalName().equals(
304 					routing.getActionClass().getCanonicalName())) {
305 				if (methodName.equals(routing.getActionMethod().getName())) {
306 					return routing;
307 				}
308 			}
309 		}
310 		throw new RoutingException(format("ECUB0103", actionClass, methodName));
311 	}
312 
313 	/**
314 	 * 指定された文字列を URL エンコードします。
315 	 * 
316 	 * @param str
317 	 *            文字列
318 	 * @param characterEncoding
319 	 *            エンコーディング
320 	 * @return エンコードされた文字列
321 	 */
322 	private static String encode(final String str,
323 			final String characterEncoding) {
324 		if (characterEncoding == null) {
325 			return str;
326 		}
327 		try {
328 			return URLBodyEncoder.encode(str, characterEncoding);
329 		} catch (final UnsupportedEncodingException e) {
330 			throw new RoutingException(e);
331 		}
332 	}
333 
334 	/**
335 	 * ルーティングのキーです。
336 	 * 
337 	 * @author baba
338 	 * @since 2.0.0
339 	 */
340 	static class RoutingKey implements Comparable<RoutingKey> {
341 
342 		private final int priority;
343 
344 		private final List<String> uriParameterNames;
345 
346 		private final Pattern pattern;
347 
348 		private final RequestMethod requestMethod;
349 
350 		private final String onSubmit;
351 
352 		public RoutingKey(final Routing routing) {
353 			this.priority = routing.getPriority();
354 			this.uriParameterNames = routing.getUriParameterNames();
355 			this.pattern = routing.getPattern();
356 			this.requestMethod = routing.getRequestMethod();
357 			this.onSubmit = routing.getOnSubmit();
358 		}
359 
360 		/**
361 		 * このキーと指定されたキーを比較します。
362 		 * <p>
363 		 * 正規表現パターンと HTTP メソッドが同じ場合は同値とみなします。
364 		 * </p>
365 		 * <p>
366 		 * また、大小関係は以下のようになります。
367 		 * <ul>
368 		 * <li>優先度(@link {@link Path#priority()})が小さい順</li>
369 		 * <li>URI 埋め込みパラメータが少ない順</li>
370 		 * <li>正規表現の順(@link {@link String#compareTo(String)})</li>
371 		 * </ul>
372 		 * </p>
373 		 * 
374 		 * @param another
375 		 *            比較対象のキー
376 		 * @return 比較結果
377 		 */
378 		public int compareTo(final RoutingKey another) {
379 			int compare = this.priority - another.priority;
380 			if (compare != 0) {
381 				return compare;
382 			}
383 			compare = this.uriParameterNames.size()
384 					- another.uriParameterNames.size();
385 			if (compare != 0) {
386 				return compare;
387 			}
388 			compare = this.pattern.pattern().compareTo(
389 					another.pattern.pattern());
390 			if (compare != 0) {
391 				return compare;
392 			}
393 			compare = this.requestMethod.compareTo(another.requestMethod);
394 			if (compare != 0) {
395 				return compare;
396 			}
397 			if (this.onSubmit == another.onSubmit) {
398 				compare = 0;
399 			} else if (this.onSubmit == null) {
400 				compare = -1;
401 			} else if (another.onSubmit == null) {
402 				compare = 1;
403 			} else {
404 				compare = this.onSubmit.compareTo(another.onSubmit);
405 			}
406 			return compare;
407 		}
408 
409 		/**
410 		 * {@inheritDoc}
411 		 */
412 		@Override
413 		public int hashCode() {
414 			final int prime = 31;
415 			int result = 1;
416 			result = prime * result
417 					+ ((onSubmit == null) ? 0 : onSubmit.hashCode());
418 			result = prime
419 					* result
420 					+ ((pattern.pattern() == null) ? 0 : pattern.pattern()
421 							.hashCode());
422 			result = prime * result + priority;
423 			result = prime * result
424 					+ ((requestMethod == null) ? 0 : requestMethod.hashCode());
425 			result = prime
426 					* result
427 					+ ((uriParameterNames == null) ? 0 : uriParameterNames
428 							.hashCode());
429 			return result;
430 		}
431 
432 		/**
433 		 * {@inheritDoc}
434 		 */
435 		@Override
436 		public boolean equals(final Object obj) {
437 			if (this == obj) {
438 				return true;
439 			}
440 			if (obj == null) {
441 				return false;
442 			}
443 			if (getClass() != obj.getClass()) {
444 				return false;
445 			}
446 			final RoutingKey other = (RoutingKey) obj;
447 			if (onSubmit == null) {
448 				if (other.onSubmit != null) {
449 					return false;
450 				}
451 			} else if (!onSubmit.equals(other.onSubmit)) {
452 				return false;
453 			}
454 			if (pattern == null) {
455 				if (other.pattern != null) {
456 					return false;
457 				}
458 			} else if (!pattern.pattern().equals(other.pattern.pattern())) {
459 				return false;
460 			}
461 			if (priority != other.priority) {
462 				return false;
463 			}
464 			if (requestMethod == null) {
465 				if (other.requestMethod != null) {
466 					return false;
467 				}
468 			} else if (!requestMethod.equals(other.requestMethod)) {
469 				return false;
470 			}
471 			if (uriParameterNames == null) {
472 				if (other.uriParameterNames != null) {
473 					return false;
474 				}
475 			} else if (!uriParameterNames.equals(other.uriParameterNames)) {
476 				return false;
477 			}
478 			return true;
479 		}
480 
481 	}
482 
483 	/**
484 	 * パスから取得した情報の実装です。
485 	 * 
486 	 * @author baba
487 	 * @since 2.0.0
488 	 */
489 	static class ResolvedPathInfo implements PathInfo {
490 
491 		/** URI から抽出したパラメータ。 */
492 		private final Map<String, String[]> uriParameters;
493 
494 		/** リクエストパラメータ名と対応するルーティングのマッピング。 */
495 		private final Map<String, Routing> routings;
496 
497 		/**
498 		 * インスタンス化します。
499 		 * 
500 		 * @param uriParameters
501 		 *            URI から抽出したパラメータ
502 		 * @param routings
503 		 *            リクエストパラメータ名とルーティングのマッピング
504 		 */
505 		public ResolvedPathInfo(final Map<String, String[]> uriParameters,
506 				final Map<String, Routing> routings) {
507 			this.uriParameters = uriParameters;
508 			this.routings = routings;
509 		}
510 
511 		/**
512 		 * {@inheritDoc}
513 		 */
514 		public Map<String, String[]> getURIParameters() {
515 			return uriParameters;
516 		}
517 
518 		/**
519 		 * {@inheritDoc}
520 		 */
521 		public Routing dispatch(final Map<String, Object[]> parameterMap) {
522 			for (final Entry<String, Routing> entry : routings.entrySet()) {
523 				if (parameterMap.containsKey(entry.getKey())) {
524 					return entry.getValue();
525 				}
526 			}
527 			return routings.get(null);
528 		}
529 
530 	}
531 
532 }