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