Coverage Report - org.seasar.cubby.routing.impl.PathResolverImpl
 
Classes in this File Line Coverage Branch Coverage Complexity
PathResolverImpl
90%
79/88
74%
25/34
0
PathResolverImpl$ActionClassCollector
86%
18/21
79%
11/14
0
PathResolverImpl$Routing
100%
19/19
100%
4/4
0
PathResolverImpl$RoutingComparator
100%
13/13
100%
8/8
0
 
 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  528
 public class PathResolverImpl implements PathResolver, Disposable {
 57  
 
 58  
         /** ロガー */
 59  1
         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  1
         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  12
         private final Comparator<Routing> routingComparator = new RoutingComparator();
 80  
 
 81  
         /** 登録されたルーティングのマップ */
 82  12
         private final Map<Routing, Routing> routings = new TreeMap<Routing, Routing>(
 83  
                         routingComparator);
 84  
 
 85  
         /** URI のエンコーディング */
 86  12
         private String uriEncoding = DEFAULT_URI_ENCODING;
 87  
 
 88  
         /**
 89  
          * インスタンス化します。
 90  
          */
 91  12
         public PathResolverImpl() {
 92  12
         }
 93  
 
 94  
         /**
 95  
          * URI エンコーディングを設定します。
 96  
          * 
 97  
          * @param uriEncoding
 98  
          *            URI エンコーディング
 99  
          */
 100  
         public void setUriEncoding(final String uriEncoding) {
 101  12
                 this.uriEncoding = uriEncoding;
 102  12
         }
 103  
 
 104  
         /**
 105  
          * 初期化します。
 106  
          */
 107  
         public void initialize() {
 108  12
                 if (!initialized) {
 109  12
                         final ClassCollector classCollector = new ActionClassCollector();
 110  12
                         classCollector.collect();
 111  
 
 112  12
                         DisposableUtil.add(this);
 113  12
                         initialized = true;
 114  
                 }
 115  12
         }
 116  
 
 117  
         /**
 118  
          * {@inheritDoc}
 119  
          */
 120  
         public void dispose() {
 121  12
                 final List<Routing> removes = new ArrayList<Routing>();
 122  12
                 for (final Routing routing : routings.keySet()) {
 123  156
                         if (routing.isAuto()) {
 124  156
                                 removes.add(routing);
 125  
                         }
 126  
                 }
 127  12
                 for (final Routing routing : removes) {
 128  156
                         routings.remove(routing);
 129  
                 }
 130  12
                 initialized = false;
 131  12
         }
 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  0
                 final Method method = ClassUtil.getMethod(actionClass, methodName,
 153  
                                 new Class<?>[0]);
 154  0
                 this.add(actionPath, actionClass, method, requestMethods, false);
 155  0
         }
 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  156
                 String uriRegex = actionPath;
 176  156
                 final List<String> uriParameterNames = new ArrayList<String>();
 177  156
                 final Matcher matcher = URI_PARAMETER_MATCHING_PATTERN
 178  
                                 .matcher(uriRegex);
 179  264
                 while (matcher.find()) {
 180  108
                         final String holder = matcher.group(2);
 181  108
                         final String[] tokens = CubbyUtils.split2(holder, ',');
 182  108
                         uriParameterNames.add(tokens[0]);
 183  
                         final String uriParameterRegex;
 184  108
                         if (tokens.length == 1) {
 185  84
                                 uriParameterRegex = DEFAULT_URI_PARAMETER_REGEX;
 186  
                         } else {
 187  24
                                 uriParameterRegex = tokens[1];
 188  
                         }
 189  108
                         uriRegex = StringUtil.replace(uriRegex, matcher.group(1),
 190  
                                         regexGroup(uriParameterRegex));
 191  108
                 }
 192  156
                 uriRegex = "^" + uriRegex + "$";
 193  156
                 final Pattern pattern = Pattern.compile(uriRegex);
 194  
 
 195  156
                 final Routing routing = new Routing(actionClass, method,
 196  
                                 uriParameterNames, pattern, requestMethods, auto);
 197  
 
 198  156
                 if (routings.containsKey(routing)) {
 199  0
                         final Routing duplication = routings.get(routing);
 200  0
                         if (!routing.getActionClass().equals(duplication.getActionClass())
 201  
                                         || !routing.getMethod().equals(duplication.getMethod())) {
 202  0
                                 throw new DuplicateRoutingRuntimeException("ECUB0001",
 203  
                                                 new Object[] { routing, duplication });
 204  
                         }
 205  0
                 } else {
 206  156
                         routings.put(routing, routing);
 207  156
                         if (logger.isDebugEnabled()) {
 208  156
                                 logger.log("DCUB0007", new Object[] { routing });
 209  
                         }
 210  
                 }
 211  156
         }
 212  
 
 213  
         /**
 214  
          * {@inheritDoc}
 215  
          */
 216  
         public InternalForwardInfo getInternalForwardInfo(final String path,
 217  
                         final String requestMethod) {
 218  12
                 initialize();
 219  
 
 220  
                 final String decodedPath;
 221  
                 try {
 222  12
                         decodedPath = URLDecoder.decode(path, uriEncoding);
 223  0
                 } catch (final IOException e) {
 224  0
                         throw new IORuntimeException(e);
 225  12
                 }
 226  
 
 227  12
                 final InternalForwardInfo internalForwardInfo = findInternalForwardInfo(
 228  
                                 decodedPath, requestMethod);
 229  12
                 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  12
                 final Map<String, String> uriParameters = new HashMap<String, String>();
 244  12
                 for (final Routing routing : routings.values()) {
 245  101
                         final Matcher matcher = routing.getPattern().matcher(path);
 246  101
                         if (matcher.find()) {
 247  12
                                 if (routing.isAcceptable(requestMethod)) {
 248  18
                                         for (int i = 0; i < matcher.groupCount(); i++) {
 249  8
                                                 final String name = routing.getUriParameterNames().get(
 250  
                                                                 i);
 251  8
                                                 final String value = matcher.group(i + 1);
 252  8
                                                 uriParameters.put(name, value);
 253  
                                         }
 254  10
                                         final String inernalFowardPath = buildInternalForwardPathWithQueryString(
 255  
                                                         routing, uriParameters);
 256  10
                                         final InternalForwardInfoImpl internalForwardInfo = new InternalForwardInfoImpl(
 257  
                                                         inernalFowardPath, routing, uriParameters);
 258  10
                                         return internalForwardInfo;
 259  
                                 }
 260  
                         }
 261  91
                 }
 262  2
                 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  10
                 final StringBuilder builder = new StringBuilder(100);
 277  10
                 builder.append(CubbyUtils.getInternalForwardPath(routing
 278  
                                 .getActionClass(), routing.getMethod().getName()));
 279  10
                 if (!uriParameters.isEmpty()) {
 280  6
                         builder.append("?");
 281  6
                         final QueryStringBuilder query = new QueryStringBuilder();
 282  6
                         final String encoding = PathResolverImpl.this.uriEncoding;
 283  6
                         if (!StringUtil.isEmpty(encoding)) {
 284  6
                                 query.setEncode(encoding);
 285  
                         }
 286  6
                         for (final Entry<String, String> entry : uriParameters.entrySet()) {
 287  8
                                 query.addParam(entry.getKey(), entry.getValue());
 288  
                         }
 289  6
                         builder.append(query.toString());
 290  
                 }
 291  10
                 return builder.toString();
 292  
         }
 293  
 
 294  
         /**
 295  
          * 命名規約を設定します。
 296  
          * 
 297  
          * @param namingConvention
 298  
          *            命名規約
 299  
          */
 300  
         public void setNamingConvention(final NamingConvention namingConvention) {
 301  12
                 this.namingConvention = namingConvention;
 302  12
         }
 303  
 
 304  
         /**
 305  
          * 指定された正規表現を括弧「()」で囲んで正規表現のグループにします。
 306  
          * 
 307  
          * @param regex
 308  
          *            正規表現
 309  
          * @return 正規表現のグループ
 310  
          */
 311  
         private static String regexGroup(final String regex) {
 312  108
                 return "(" + regex + ")";
 313  
         }
 314  
 
 315  
         /**
 316  
          * ルーティングのコンパレータ。
 317  
          * 
 318  
          * @author baba
 319  
          */
 320  1151
         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  1139
                         int compare = routing1.getUriParameterNames().size()
 343  
                                         - routing2.getUriParameterNames().size();
 344  1139
                         if (compare != 0) {
 345  462
                                 return compare;
 346  
                         }
 347  677
                         compare = routing1.getPattern().pattern().compareTo(
 348  
                                         routing2.getPattern().pattern());
 349  677
                         if (compare != 0) {
 350  446
                                 return compare;
 351  
                         }
 352  231
                         final RequestMethod[] requestMethods1 = routing1
 353  
                                         .getRequestMethods();
 354  231
                         final RequestMethod[] requestMethods2 = routing2
 355  
                                         .getRequestMethods();
 356  400
                         for (final RequestMethod requestMethod : requestMethods1) {
 357  315
                                 if (ArrayUtil.contains(requestMethods2, requestMethod)) {
 358  146
                                         return 0;
 359  
                                 }
 360  
                         }
 361  85
                         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  170
                                 final boolean auto) {
 411  170
                         this.actionClass = actionClass;
 412  170
                         this.method = method;
 413  170
                         this.uriParameterNames = uriParameterNames;
 414  170
                         this.pattern = pattern;
 415  170
                         this.requestMethods = requestMethods;
 416  170
                         this.auto = auto;
 417  170
                 }
 418  
 
 419  
                 /**
 420  
                  * アクションクラスを取得します。
 421  
                  * 
 422  
                  * @return アクションクラス
 423  
                  */
 424  
                 public Class<? extends Action> getActionClass() {
 425  20
                         return actionClass;
 426  
                 }
 427  
 
 428  
                 /**
 429  
                  * メソッドを取得します。
 430  
                  * 
 431  
                  * @return メソッド
 432  
                  */
 433  
                 public Method getMethod() {
 434  20
                         return method;
 435  
                 }
 436  
 
 437  
                 /**
 438  
                  * URI パラメータ名を取得します。
 439  
                  * 
 440  
                  * @return URI パラメータ名
 441  
                  */
 442  
                 public List<String> getUriParameterNames() {
 443  2286
                         return uriParameterNames;
 444  
                 }
 445  
 
 446  
                 /**
 447  
                  * 正規表現パターンを取得します。
 448  
                  * 
 449  
                  * @return 正規表現パターン
 450  
                  */
 451  
                 public Pattern getPattern() {
 452  1455
                         return pattern;
 453  
                 }
 454  
 
 455  
                 /**
 456  
                  * リクエストメソッドを取得します。
 457  
                  * 
 458  
                  * @return リクエストメソッド
 459  
                  */
 460  
                 public RequestMethod[] getRequestMethods() {
 461  462
                         return requestMethods;
 462  
                 }
 463  
 
 464  
                 /**
 465  
                  * 自動登録されたルーティングかを示します。
 466  
                  * 
 467  
                  * @return 自動登録されたルーティングの場合は <code>true</code>、そうでない場合は
 468  
                  *         <code>false</code>
 469  
                  */
 470  
                 public boolean isAuto() {
 471  156
                         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  14
                         for (final RequestMethod acceptableRequestMethod : requestMethods) {
 483  12
                                 if (StringUtil.equalsIgnoreCase(acceptableRequestMethod.name(),
 484  
                                                 requestMethod)) {
 485  10
                                         return true;
 486  
                                 }
 487  
                         }
 488  2
                         return false;
 489  
                 }
 490  
 
 491  
                 /**
 492  
                  * このオブジェクトの文字列表現を返します。
 493  
                  * 
 494  
                  * @return このオブジェクトの正規表現
 495  
                  */
 496  
                 @Override
 497  
                 public String toString() {
 498  161
                         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  12
                 public ActionClassCollector() {
 518  12
                         super(namingConvention);
 519  12
                 }
 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  264
                         if (shortClassName.indexOf('$') != -1) {
 532  120
                                 return;
 533  
                         }
 534  144
                         final String className = ClassUtil.concatName(packageName,
 535  
                                         shortClassName);
 536  144
                         if (!namingConvention.isTargetClassName(className)) {
 537  0
                                 return;
 538  
                         }
 539  144
                         if (!className.endsWith(namingConvention.getActionSuffix())) {
 540  108
                                 return;
 541  
                         }
 542  36
                         final Class<? extends Action> clazz = classForName(className);
 543  36
                         if (!CubbyUtils.isActionClass(clazz)) {
 544  0
                                 return;
 545  
                         }
 546  36
                         if (namingConvention.isSkipClass(clazz)) {
 547  0
                                 return;
 548  
                         }
 549  
 
 550  768
                         for (final Method method : clazz.getMethods()) {
 551  732
                                 if (CubbyUtils.isActionMethod(method)) {
 552  156
                                         final String actionPath = CubbyUtils.getActionPath(clazz,
 553  
                                                         method);
 554  156
                                         final RequestMethod[] acceptableRequestMethods = CubbyUtils
 555  
                                                         .getAcceptableRequestMethods(clazz, method);
 556  156
                                         add(actionPath, clazz, method, acceptableRequestMethods,
 557  
                                                         true);
 558  
                                 }
 559  
                         }
 560  36
                 }
 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  36
                 return ClassUtil.forName(className);
 576  
         }
 577  
 
 578  
 }