@SuppressWarnings({"unchecked"})
  private String createURLInternal(Map paramValues, String encoding, boolean includeContextPath) {

    if (encoding == null) encoding = "utf-8";

    String contextPath = "";
    if (includeContextPath) {
      GrailsWebRequest webRequest = (GrailsWebRequest) RequestContextHolder.getRequestAttributes();
      if (webRequest != null) {
        contextPath = webRequest.getAttributes().getApplicationUri(webRequest.getCurrentRequest());
      }
    }
    if (paramValues == null) paramValues = Collections.EMPTY_MAP;
    StringBuilder uri = new StringBuilder(contextPath);
    Set usedParams = new HashSet();

    String[] tokens = urlData.getTokens();
    int paramIndex = 0;
    for (int i = 0; i < tokens.length; i++) {
      String token = tokens[i];
      if (i == tokens.length - 1 && urlData.hasOptionalExtension()) {
        token += OPTIONAL_EXTENSION_WILDCARD;
      }
      Matcher m = OPTIONAL_EXTENSION_WILDCARD_PATTERN.matcher(token);
      if (m.find()) {

        if (token.startsWith(CAPTURED_WILDCARD)) {
          ConstrainedProperty prop = constraints[paramIndex++];
          String propName = prop.getPropertyName();

          Object value = paramValues.get(propName);
          usedParams.add(propName);

          if (value != null) {
            token = token.replaceFirst(DOUBLE_WILDCARD_PATTERN.pattern(), value.toString());
          } else if (prop.isNullable()) {
            break;
          }
        }
        uri.append(SLASH);
        ConstrainedProperty prop = constraints[paramIndex++];
        String propName = prop.getPropertyName();
        Object value = paramValues.get(propName);
        usedParams.add(propName);
        if (value != null) {
          String ext = "." + value;
          uri.append(
              token
                  .replace(OPTIONAL_EXTENSION_WILDCARD + '?', ext)
                  .replace(OPTIONAL_EXTENSION_WILDCARD, ext));
        } else {
          uri.append(
              token
                  .replace(OPTIONAL_EXTENSION_WILDCARD + '?', "")
                  .replace(OPTIONAL_EXTENSION_WILDCARD, ""));
        }

        continue;
      }
      if (token.endsWith("?")) {
        token = token.substring(0, token.length() - 1);
      }
      m = DOUBLE_WILDCARD_PATTERN.matcher(token);
      if (m.find()) {
        StringBuffer buf = new StringBuffer();
        do {
          ConstrainedProperty prop = constraints[paramIndex++];
          String propName = prop.getPropertyName();
          Object value = paramValues.get(propName);
          usedParams.add(propName);
          if (value == null && !prop.isNullable()) {
            throw new UrlMappingException(
                "Unable to create URL for mapping ["
                    + this
                    + "] and parameters ["
                    + paramValues
                    + "]. Parameter ["
                    + prop.getPropertyName()
                    + "] is required, but was not specified!");
          } else if (value == null) {
            m.appendReplacement(buf, "");
          } else {
            m.appendReplacement(buf, Matcher.quoteReplacement(value.toString()));
          }
        } while (m.find());

        m.appendTail(buf);

        try {
          String v = buf.toString();
          if (v.indexOf(SLASH) > -1 && CAPTURED_DOUBLE_WILDCARD.equals(token)) {
            // individually URL encode path segments
            if (v.startsWith(SLASH)) {
              // get rid of leading slash
              v = v.substring(SLASH.length());
            }
            String[] segs = v.split(SLASH);
            for (String segment : segs) {
              uri.append(SLASH).append(encode(segment, encoding));
            }
          } else if (v.length() > 0) {
            // original behavior
            uri.append(SLASH).append(encode(v, encoding));
          } else {
            // Stop processing tokens once we hit an empty one.
            break;
          }
        } catch (UnsupportedEncodingException e) {
          throw new ControllerExecutionException(
              "Error creating URL for parameters ["
                  + paramValues
                  + "], problem encoding URL part ["
                  + buf
                  + "]: "
                  + e.getMessage(),
              e);
        }
      } else {
        uri.append(SLASH).append(token);
      }
    }
    populateParameterList(paramValues, encoding, uri, usedParams);

    if (LOG.isDebugEnabled()) {
      LOG.debug(
          "Created reverse URL mapping ["
              + uri.toString()
              + "] for parameters ["
              + paramValues
              + "]");
    }
    return uri.toString();
  }
  @SuppressWarnings("unchecked")
  private UrlMappingInfo createUrlMappingInfo(String uri, Matcher m) {
    boolean hasOptionalExtension = urlData.hasOptionalExtension();
    Map params = new HashMap();
    Errors errors = new MapBindingResult(params, "urlMapping");
    int groupCount = m.groupCount();
    String lastGroup = null;
    for (int i = 0; i < groupCount; i++) {
      lastGroup = m.group(i + 1);
      // if null optional.. ignore
      if (i == groupCount - 1 && hasOptionalExtension) {
        ConstrainedProperty cp = constraints[constraints.length - 1];
        cp.validate(this, lastGroup, errors);

        if (errors.hasErrors()) {
          return null;
        }

        params.put(cp.getPropertyName(), lastGroup);
        break;
      } else {
        if (lastGroup == null) continue;
        int j = lastGroup.indexOf('?');
        if (j > -1) {
          lastGroup = lastGroup.substring(0, j);
        }
        if (constraints.length > i) {
          ConstrainedProperty cp = constraints[i];
          cp.validate(this, lastGroup, errors);

          if (errors.hasErrors()) {
            return null;
          }

          params.put(cp.getPropertyName(), lastGroup);
        }
      }
    }

    for (Object key : parameterValues.keySet()) {
      params.put(key, parameterValues.get(key));
    }

    if (controllerName == null) {
      controllerName =
          createRuntimeConstraintEvaluator(GrailsControllerClass.CONTROLLER, constraints);
    }

    if (actionName == null) {
      actionName = createRuntimeConstraintEvaluator(GrailsControllerClass.ACTION, constraints);
    }

    if (namespace == null) {
      namespace = createRuntimeConstraintEvaluator(NAMESPACE, constraints);
    }

    if (viewName == null) {
      viewName = createRuntimeConstraintEvaluator(GrailsControllerClass.VIEW, constraints);
    }

    if (redirectInfo == null) {
      redirectInfo = createRuntimeConstraintEvaluator("redirect", constraints);
    }

    DefaultUrlMappingInfo info;
    if (forwardURI != null && controllerName == null) {
      info = new DefaultUrlMappingInfo(forwardURI, getHttpMethod(), urlData, servletContext);
    } else if (viewName != null && controllerName == null) {
      info = new DefaultUrlMappingInfo(viewName, params, urlData, servletContext);
    } else {
      info =
          new DefaultUrlMappingInfo(
              redirectInfo,
              controllerName,
              actionName,
              namespace,
              pluginName,
              getViewName(),
              getHttpMethod(),
              getVersion(),
              params,
              urlData,
              servletContext);
    }

    if (parseRequest) {
      info.setParsingRequest(parseRequest);
    }

    return info;
  }
  private void parse(UrlMappingData data, ConstrainedProperty[] constraints) {
    Assert.notNull(data, "Argument [data] cannot be null");

    String[] urls = data.getLogicalUrls();
    urlData = data;
    patterns = new Pattern[urls.length];

    for (int i = 0; i < urls.length; i++) {
      String url = urls[i];
      Integer slashCount = org.springframework.util.StringUtils.countOccurrencesOf(url, "/");
      List<Pattern> tokenCountPatterns = patternByTokenCount.get(slashCount);
      if (tokenCountPatterns == null) {
        tokenCountPatterns = new ArrayList<Pattern>();
        patternByTokenCount.put(slashCount, tokenCountPatterns);
      }

      Pattern pattern = convertToRegex(url);
      if (pattern == null) {
        throw new IllegalStateException(
            "Cannot use null pattern in regular expression mapping for url ["
                + data.getUrlPattern()
                + "]");
      }
      tokenCountPatterns.add(pattern);
      this.patterns[i] = pattern;
    }

    if (constraints != null) {
      String[] tokens = data.getTokens();
      int pos = 0;
      int currentToken = 0;
      int tokensLength = tokens.length - 1;
      int constraintUpperBound = constraints.length;
      if (data.hasOptionalExtension()) {
        constraintUpperBound--;
        constraints[constraintUpperBound].setNullable(true);
      }

      for (int i = 0; i < constraintUpperBound; i++) {
        ConstrainedProperty constraint = constraints[i];
        if (currentToken > tokensLength) break;
        String token = tokens[currentToken];
        int shiftLength = 3;
        pos = token.indexOf(CAPTURED_WILDCARD, pos);
        while (pos == -1) {
          boolean isLastToken = currentToken == tokensLength - 1;
          if (currentToken < tokensLength) {

            token = tokens[++currentToken];
            // special handling for last token to deal with optional extension
            if (isLastToken) {
              if (token.startsWith(CAPTURED_WILDCARD + '?')) {
                constraint.setNullable(true);
              }
              if (token.endsWith(OPTIONAL_EXTENSION_WILDCARD + '?')) {
                constraints[constraints.length - 1].setNullable(true);
              }
            } else {
              pos = token.indexOf(CAPTURED_WILDCARD, pos);
            }
          } else {
            break;
          }
        }

        if (pos != -1
            && pos + shiftLength < token.length()
            && token.charAt(pos + shiftLength) == '?') {
          constraint.setNullable(true);
        }

        // Move on to the next place-holder.
        pos += shiftLength;
        if (token.indexOf(CAPTURED_WILDCARD, pos) == -1) {
          currentToken++;
          pos = 0;
        }
      }
    }
  }