content: /**
   * Copyright 2014 55 Minutes (http://www.55minutes.com)
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *    http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  package fiftyfive.wicket.mapper;
  
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.List;
  import java.util.regex.Pattern;
  
  import org.apache.wicket.request.Request;
  import org.apache.wicket.request.component.IRequestablePage;
  import org.apache.wicket.request.mapper.MountedMapper;
  import org.apache.wicket.request.mapper.parameter.IPageParametersEncoder;
  import org.apache.wicket.request.mapper.parameter.PageParameters;
  import org.apache.wicket.request.mapper.parameter.PageParametersEncoder;
  import org.apache.wicket.util.ClassProvider;
  import org.apache.wicket.util.string.StringValue;
  
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  
  /**
   * An improved version of Wicket's standard {@link MountedMapper} that additionally allows
   * regular expressions inside placeholders. This feature is inspired by the pattern matching
   * behavior of the JAX-RS {@code @Path} annotation.
   * <pre class="example">
   * mount(new PatternMountedMapper("people/${personId:\\d+}", PersonPage.class));</pre>
   * This will map URLs like {@code people/12345} but yield a 404 not found for something like
   * {@code people/abc} since {@code abc} doesn't match the {@code \d+} regular expression.
   * 
   * @since 3.0
   */
  public class PatternMountedMapper extends MountedMapper
  {
      private static final Logger LOGGER = LoggerFactory.getLogger(PatternMountedMapper.class);
  
      private final int numSegments;
      private final List<PatternPlaceholder> patternPlaceholders;
      private boolean exact = false;
  
      /**
       * @see MountedMapper#MountedMapper(String, Class)
       */
      public PatternMountedMapper(String mountPath, Class<? extends IRequestablePage> pageClass)
      {
          this(mountPath, pageClass, new PageParametersEncoder());
      }
  
      /**
       * @see MountedMapper#MountedMapper(String, ClassProvider)
       */
      public PatternMountedMapper(String mountPath,
                                  ClassProvider<? extends IRequestablePage> pageClassProvider)
      {
          this(mountPath, pageClassProvider, new PageParametersEncoder());
      }
  
  
      /**
       * @see MountedMapper#MountedMapper(String, Class, IPageParametersEncoder)
       */
      public PatternMountedMapper(String mountPath,
                                  Class<? extends IRequestablePage> pageClass,
                                  IPageParametersEncoder pageParametersEncoder)
      {
          this(mountPath, ClassProvider.of(pageClass), pageParametersEncoder);
      }
  
      /**
       * @see MountedMapper#MountedMapper(String, ClassProvider, IPageParametersEncoder)
       */
      public PatternMountedMapper(String mountPath,
                                  ClassProvider<? extends IRequestablePage> pageClassProvider,
                                  IPageParametersEncoder pageParametersEncoder)
      {
          super(removePatternsFromPlaceholders(mountPath), pageClassProvider, pageParametersEncoder);
  
          String[] segments = getMountSegments(mountPath);
          this.numSegments = segments.length;
          this.patternPlaceholders = new ArrayList<PatternPlaceholder>(1);
          for(String seg: segments)
          {
              String placeholder = getPlaceholder(seg);
              if(placeholder != null)
              {
                  this.patternPlaceholders.add(new PatternPlaceholder(placeholder));
              }
          }
      }
      
      /**
       * Set to {@code true}, to force this mapper to strictly match URLs by disallowing any extra
       * path elements that come after the matched pattern.
       * <pre class="example">
       * PatternMountedMapper m = new PatternMountedMapper(MyPage.class, "page/${id:\\d+}");
       * // These will always be matched: "page/1", "page/2", "page/30", etc.
       * // By default, these will be matched as well: "page/1/whatever/foo/bar", "page/2/baz"
       * m.setExact(true);
       * // Now these will not be matched: "page/1/whatever/foo/bar", "page/2/baz"</pre>
       * 
       * In other words, if {@code exact} is set to {@code false}, extra path elements after the
       * specified pattern will be allowed. The default is {@code false}, to match the default
       * behavior of Wicket's {@link MountedMapper}.
       * 
       * @return {@code this} to allow chaining
       */
      public PatternMountedMapper setExact(boolean exact)
      {
          this.exact = exact;
          return this;
      }
      
      /**
       * First delegate to the superclass to parse the request as normal, then additionally
       * verify that all regular expressions specified in the placeholders match.
       */
      @Override
      protected UrlInfo parseRequest(Request request)
      {
          // Parse the request normally. If the standard impl can't parse it, we won't either.
          UrlInfo info = super.parseRequest(request);
          if(null == info || null == info.getPageParameters())
          {
              return info;
          }
          
          // If exact matching, reject URLs that have more than expected number of segments
          if(exact)
          {
              int requestNumSegments = request.getUrl().getSegments().size();
              if(requestNumSegments > this.numSegments)
              {
                  return null;
              }
          }
          
          // Loop through each placeholder and verify that the regex of the placeholder matches
          // the value that was provided in the request url. If any of the values don't match,
          // immediately return null signifying that the url is not matched by this mapper.
          PageParameters params = info.getPageParameters();
          for(PatternPlaceholder pp : getPatternPlaceholders())
          {
              List<StringValue> values = params.getValues(pp.getName());
              if(null == values || values.size() == 0)
              {
                  values = Arrays.asList(StringValue.valueOf(""));
              }
              for(StringValue val : values)
              {
                  if(!pp.matches(val.toString()))
                  {
                      if(LOGGER.isDebugEnabled())
                      {
                          LOGGER.debug(String.format(
                              "Parameter \"%s\" did not match pattern placeholder %s", val, pp));
                      }
                      return null;
                  }
              }
          }
          return info;
      }
      
      /**
       * The list of placeholders (in other words, the <code>${name:regex}</code> components of the
       * mount path).
       */
      protected List<PatternPlaceholder> getPatternPlaceholders()
      {
          return this.patternPlaceholders;
      }
      
      /**
       * Remove the regular expression portion of all placeholders from the given path so that
       * the standard {@link MountedMapper} isn't confused by them. This allows us to reuse all 
       * the existing code of the superclass. This method must be static because we need to call it
       * before invoking the superclass constructor.
       * <pre class="example">
       * removePatternsFromPlaceholders("people/${personId:\\d+}");
       * // "people/${personId}"</pre>
       */
      protected static String removePatternsFromPlaceholders(String path)
      {
          if(null == path)
          {
              return null;
          }
          StringBuilder result = new StringBuilder();
          String [] segments = path.split("/");
          for(int i=0; i<segments.length; i++)
          {
              String seg = segments[i];
              result.append(seg.replaceAll("^(\\$\\{[^:]+):.+\\}$", "$1}"));
              if(i < segments.length-1 || path.endsWith("/"))
              {
                  result.append("/");
              }
          }
          return result.toString();
      }
      
      /**
       * Represents a placeholder that optionally contains a regular expression.
       */
      protected static class PatternPlaceholder
      {
          private final String placeholder;
          private final String pattern;
          private final String name;
          
          public PatternPlaceholder(String placeholder)
          {
              this.placeholder = placeholder;
  
              int colon = placeholder.indexOf(":");
              if(colon > 0 && colon < placeholder.length() - 2)
              {
                  this.name = placeholder.substring(0, colon);
                  this.pattern = placeholder.substring(colon + 1);
              }
              else
              {
                  this.name = placeholder;
                  this.pattern = null;
              }
          }
          
          /**
           * Return {@code true} if this placeholder has a regex pattern and that pattern matches
           * the specified value. If this placeholder doesn't have a regex, always return
           * {@code true} always.
           */
          public boolean matches(CharSequence value)
          {
              return null == this.pattern || Pattern.matches(this.pattern, value);
          }
          
          /**
           * The name of this placeholder with the {@code :regex} portion removed.
           */
          public String getName()
          {
              return this.name;
          }
          
          /**
           * For debugging.
           */
          public String toString()
          {
              return "${" + this.placeholder + "}";
          }
      }
  }
  