View Javadoc

1   /*
2    * Copyright 2004-2010 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  
17  package org.seasar.cubby.routing.impl;
18  
19  import static org.seasar.cubby.internal.util.LogMessages.format;
20  
21  import java.io.UnsupportedEncodingException;
22  import java.lang.reflect.Method;
23  import java.util.ArrayList;
24  import java.util.Collection;
25  import java.util.HashMap;
26  import java.util.Iterator;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.TreeMap;
30  import java.util.Map.Entry;
31  import java.util.regex.Matcher;
32  import java.util.regex.Pattern;
33  
34  import org.seasar.cubby.action.Path;
35  import org.seasar.cubby.action.RequestMethod;
36  import org.seasar.cubby.internal.util.MetaUtils;
37  import org.seasar.cubby.internal.util.QueryStringBuilder;
38  import org.seasar.cubby.internal.util.URLBodyEncoder;
39  import org.seasar.cubby.routing.PathInfo;
40  import org.seasar.cubby.routing.PathResolver;
41  import org.seasar.cubby.routing.PathTemplateParser;
42  import org.seasar.cubby.routing.Routing;
43  import org.seasar.cubby.routing.RoutingException;
44  import org.seasar.cubby.util.ActionUtils;
45  import org.slf4j.Logger;
46  import org.slf4j.LoggerFactory;
47  
48  /**
49   * パスに対応するアクションメソッドを解決するためのクラスの実装です。
50   * 
51   * @author baba
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 final 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 	 */
339 	static class RoutingKey implements Comparable<RoutingKey> {
340 
341 		private final int priority;
342 
343 		private final List<String> uriParameterNames;
344 
345 		private final Pattern pattern;
346 
347 		private final RequestMethod requestMethod;
348 
349 		private final String onSubmit;
350 
351 		public RoutingKey(final Routing routing) {
352 			this.priority = routing.getPriority();
353 			this.uriParameterNames = routing.getUriParameterNames();
354 			this.pattern = routing.getPattern();
355 			this.requestMethod = routing.getRequestMethod();
356 			this.onSubmit = routing.getOnSubmit();
357 		}
358 
359 		/**
360 		 * このキーと指定されたキーを比較します。
361 		 * <p>
362 		 * 正規表現パターンと HTTP メソッドが同じ場合は同値とみなします。
363 		 * </p>
364 		 * <p>
365 		 * また、大小関係は以下のようになります。
366 		 * <ul>
367 		 * <li>優先度(@link {@link Path#priority()})が小さい順</li>
368 		 * <li>URI 埋め込みパラメータが少ない順</li>
369 		 * <li>正規表現の順(@link {@link String#compareTo(String)})</li>
370 		 * </ul>
371 		 * </p>
372 		 * 
373 		 * @param another
374 		 *            比較対象のキー
375 		 * @return 比較結果
376 		 */
377 		public int compareTo(final RoutingKey another) {
378 			int compare = this.priority - another.priority;
379 			if (compare != 0) {
380 				return compare;
381 			}
382 			compare = this.uriParameterNames.size()
383 					- another.uriParameterNames.size();
384 			if (compare != 0) {
385 				return compare;
386 			}
387 			compare = this.pattern.pattern().compareTo(
388 					another.pattern.pattern());
389 			if (compare != 0) {
390 				return compare;
391 			}
392 			compare = this.requestMethod.compareTo(another.requestMethod);
393 			if (compare != 0) {
394 				return compare;
395 			}
396 			if (this.onSubmit == another.onSubmit) {
397 				compare = 0;
398 			} else if (this.onSubmit == null) {
399 				compare = -1;
400 			} else if (another.onSubmit == null) {
401 				compare = 1;
402 			} else {
403 				compare = this.onSubmit.compareTo(another.onSubmit);
404 			}
405 			return compare;
406 		}
407 
408 		/**
409 		 * {@inheritDoc}
410 		 */
411 		@Override
412 		public int hashCode() {
413 			final int prime = 31;
414 			int result = 1;
415 			result = prime * result
416 					+ ((onSubmit == null) ? 0 : onSubmit.hashCode());
417 			result = prime
418 					* result
419 					+ ((pattern.pattern() == null) ? 0 : pattern.pattern()
420 							.hashCode());
421 			result = prime * result + priority;
422 			result = prime * result
423 					+ ((requestMethod == null) ? 0 : requestMethod.hashCode());
424 			result = prime
425 					* result
426 					+ ((uriParameterNames == null) ? 0 : uriParameterNames
427 							.hashCode());
428 			return result;
429 		}
430 
431 		/**
432 		 * {@inheritDoc}
433 		 */
434 		@Override
435 		public boolean equals(final Object obj) {
436 			if (this == obj) {
437 				return true;
438 			}
439 			if (obj == null) {
440 				return false;
441 			}
442 			if (getClass() != obj.getClass()) {
443 				return false;
444 			}
445 			final RoutingKey other = (RoutingKey) obj;
446 			if (onSubmit == null) {
447 				if (other.onSubmit != null) {
448 					return false;
449 				}
450 			} else if (!onSubmit.equals(other.onSubmit)) {
451 				return false;
452 			}
453 			if (pattern == null) {
454 				if (other.pattern != null) {
455 					return false;
456 				}
457 			} else if (!pattern.pattern().equals(other.pattern.pattern())) {
458 				return false;
459 			}
460 			if (priority != other.priority) {
461 				return false;
462 			}
463 			if (requestMethod == null) {
464 				if (other.requestMethod != null) {
465 					return false;
466 				}
467 			} else if (!requestMethod.equals(other.requestMethod)) {
468 				return false;
469 			}
470 			if (uriParameterNames == null) {
471 				if (other.uriParameterNames != null) {
472 					return false;
473 				}
474 			} else if (!uriParameterNames.equals(other.uriParameterNames)) {
475 				return false;
476 			}
477 			return true;
478 		}
479 
480 	}
481 
482 	/**
483 	 * パスから取得した情報の実装です。
484 	 * 
485 	 * @author baba
486 	 */
487 	static class ResolvedPathInfo implements PathInfo {
488 
489 		/** URI から抽出したパラメータ。 */
490 		private final Map<String, String[]> uriParameters;
491 
492 		/** 要求パラメータ名と対応するルーティングのマッピング。 */
493 		private final Map<String, Routing> routings;
494 
495 		/**
496 		 * インスタンス化します。
497 		 * 
498 		 * @param uriParameters
499 		 *            URI から抽出したパラメータ
500 		 * @param routings
501 		 *            要求パラメータ名とルーティングのマッピング
502 		 */
503 		public ResolvedPathInfo(final Map<String, String[]> uriParameters,
504 				final Map<String, Routing> routings) {
505 			this.uriParameters = uriParameters;
506 			this.routings = routings;
507 		}
508 
509 		/**
510 		 * {@inheritDoc}
511 		 */
512 		public Map<String, String[]> getURIParameters() {
513 			return uriParameters;
514 		}
515 
516 		/**
517 		 * {@inheritDoc}
518 		 */
519 		public Routing dispatch(final Map<String, Object[]> parameterMap) {
520 			for (final Entry<String, Routing> entry : routings.entrySet()) {
521 				if (parameterMap.containsKey(entry.getKey())) {
522 					return entry.getValue();
523 				}
524 			}
525 			return routings.get(null);
526 		}
527 
528 	}
529 
530 }