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 }