FREEMARKER-55: Adding url function
Project: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/commit/8bdc5fdc Tree: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/tree/8bdc5fdc Diff: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/diff/8bdc5fdc Branch: refs/heads/3 Commit: 8bdc5fdcf6507a00113e8f396cdf9a90c8c3857c Parents: 6f025c7 Author: Woonsan Ko <woon...@apache.org> Authored: Mon Sep 11 15:45:46 2017 -0400 Committer: Woonsan Ko <woon...@apache.org> Committed: Mon Sep 11 15:45:46 2017 -0400 ---------------------------------------------------------------------- .../freemarker/spring/model/UrlFunction.java | 189 ++++++++++++++++++- 1 file changed, 179 insertions(+), 10 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/8bdc5fdc/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/UrlFunction.java ---------------------------------------------------------------------- diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/UrlFunction.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/UrlFunction.java index 402e74d..fe1b1fd 100644 --- a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/UrlFunction.java +++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/UrlFunction.java @@ -19,6 +19,14 @@ package org.apache.freemarker.spring.model; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -27,9 +35,16 @@ import org.apache.freemarker.core.Environment; import org.apache.freemarker.core.TemplateException; import org.apache.freemarker.core.model.ArgumentArrayLayout; import org.apache.freemarker.core.model.ObjectWrapperAndUnwrapper; +import org.apache.freemarker.core.model.TemplateHashModelEx2; +import org.apache.freemarker.core.model.TemplateHashModelEx2.KeyValuePairIterator; import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateStringModel; +import org.apache.freemarker.core.util.CallableUtils; import org.apache.freemarker.core.util.StringToIndexMap; +import org.apache.freemarker.core.util._KeyValuePair; import org.springframework.web.servlet.support.RequestContext; +import org.springframework.web.servlet.support.RequestDataValueProcessor; +import org.springframework.web.util.UriUtils; /** * A <code>TemplateFunctionModel</code> providing functionality equivalent to the Spring Framework's @@ -51,20 +66,21 @@ public class UrlFunction extends AbstractSpringTemplateFunctionModel { private static final int VALUE_PARAM_IDX = 0; private static final int CONTEXT_PARAM_IDX = 1; + private static final int PARAMS_PARAM_IDX = 2; private static final String CONTEXT_PARAM_NAME = "context"; + /** + * Absolute URL pattern. e.g, http(s)://example.com, mailto:j...@example.com, tel:123-456-7890. + */ + private static final Pattern ABS_URL_PATTERN = Pattern.compile("^((([A-Za-z]+?:)?\\/\\/)|[A-Za-z]+:)[\\w.-]+"); - // TODO: How to deal with parameters (spring:param tags)?? + private static final String URL_TEMPLATE_DELIMITER_PREFIX = "{"; + private static final String URL_TEMPLATE_DELIMITER_SUFFIX = "}"; - private static final ArgumentArrayLayout ARGS_LAYOUT = - ArgumentArrayLayout.create( - 1, - false, - StringToIndexMap.of(CONTEXT_PARAM_NAME, CONTEXT_PARAM_IDX), - false - ); + private static final ArgumentArrayLayout ARGS_LAYOUT = ArgumentArrayLayout.create(1, false, + StringToIndexMap.of(CONTEXT_PARAM_NAME, CONTEXT_PARAM_IDX), true); public UrlFunction(HttpServletRequest request, HttpServletResponse response) { super(request, response); @@ -74,8 +90,75 @@ public class UrlFunction extends AbstractSpringTemplateFunctionModel { public TemplateModel executeInternal(TemplateModel[] args, CallPlace callPlace, Environment env, ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper, RequestContext requestContext) throws TemplateException { - // TODO - return null; + final String value = CallableUtils.getStringArgument(args, VALUE_PARAM_IDX, this); + final String context = CallableUtils.getOptionalStringArgument(args, CONTEXT_PARAM_IDX, this); + + List<_KeyValuePair<String, String>> params = null; + final TemplateHashModelEx2 paramsHashModel = (TemplateHashModelEx2) args[PARAMS_PARAM_IDX]; + + if (!paramsHashModel.isEmptyHash()) { + params = new ArrayList<>(); + + TemplateHashModelEx2.KeyValuePair pair; + TemplateModel paramNameModel; + TemplateModel paramValueModel; + String paramName; + String paramValue; + + for (KeyValuePairIterator pairIt = paramsHashModel.keyValuePairIterator(); pairIt.hasNext();) { + pair = pairIt.next(); + paramNameModel = pair.getKey(); + paramValueModel = pair.getValue(); + + if ((paramNameModel instanceof TemplateStringModel) + && (paramValueModel instanceof TemplateStringModel)) { + paramName = ((TemplateStringModel) paramNameModel).getAsString(); + paramValue = ((TemplateStringModel) paramValueModel).getAsString(); + + if (paramName.isEmpty()) { + CallableUtils.newArgumentValueException(PARAMS_PARAM_IDX, + "Parameter name must be a non-blank string.", this); + } + + params.add(new _KeyValuePair<String, String>(paramName, paramValue)); + } else { + CallableUtils.newArgumentValueException(PARAMS_PARAM_IDX, + "Parameter name and value must be string.", this); + } + } + } + + final UrlType urlType = determineUrlType(value); + + StringBuilder urlBuilder = new StringBuilder(); + + if (urlType == UrlType.CONTEXT_RELATIVE) { + if (context == null) { + urlBuilder.append(getRequest().getContextPath()); + } else if (context.endsWith("/")) { + urlBuilder.append(context.substring(0, context.length() - 1)); + } else { + urlBuilder.append(context); + } + } + + Set<String> templateParams = new HashSet<>(); + urlBuilder.append(replaceUriTemplateParams(value, params, templateParams)); + urlBuilder.append(createQueryString(params, templateParams, (urlBuilder.indexOf("?") == -1))); + + String urlString = urlBuilder.toString(); + + if (urlType != UrlType.ABSOLUTE) { + urlString = getResponse().encodeURL(urlString); + } + + RequestDataValueProcessor processor = requestContext.getRequestDataValueProcessor(); + + if ((processor != null) && (getRequest() instanceof HttpServletRequest)) { + urlString = processor.processUrl(getRequest(), urlString); + } + + return wrapObject(objectWrapperAndUnwrapper, urlString); } @Override @@ -83,4 +166,90 @@ public class UrlFunction extends AbstractSpringTemplateFunctionModel { return ARGS_LAYOUT; } + private UrlType determineUrlType(final String value) { + Matcher m = ABS_URL_PATTERN.matcher(value); + + if (m.matches()) { + return UrlType.ABSOLUTE; + } else if (value.startsWith("/")) { + return UrlType.CONTEXT_RELATIVE; + } else { + return UrlType.RELATIVE; + } + } + + private String replaceUriTemplateParams(String uri, List<_KeyValuePair<String, String>> params, Set<String> usedParams) + throws TemplateException { + final String encoding = getResponse().getCharacterEncoding(); + + String paramName; + String paramValue; + + for (_KeyValuePair<String, String> pair : params) { + paramName = pair.getKey(); + paramValue = pair.getValue(); + + String template = URL_TEMPLATE_DELIMITER_PREFIX + paramName + URL_TEMPLATE_DELIMITER_SUFFIX; + + if (uri.contains(template)) { + usedParams.add(paramName); + + try { + uri = uri.replace(template, UriUtils.encodePath(paramValue, encoding)); + } catch (UnsupportedEncodingException e) { + CallableUtils.newGenericExecuteException("Cannot encode URI. " + e, this); + } + } else { + template = URL_TEMPLATE_DELIMITER_PREFIX + '/' + paramName + URL_TEMPLATE_DELIMITER_SUFFIX; + + if (uri.contains(template)) { + usedParams.add(paramName); + + try { + uri = uri.replace(template, UriUtils.encodePathSegment(paramValue, encoding)); + } catch (UnsupportedEncodingException e) { + CallableUtils.newGenericExecuteException("Cannot encode URI. " + e, this); + } + } + } + } + + return uri; + } + + private String createQueryString(List<_KeyValuePair<String, String>> params, Set<String> usedParams, boolean includeQueryStringDelimiter) + throws TemplateException { + final String encoding = getResponse().getCharacterEncoding(); + final StringBuilder queryStringBuilder = new StringBuilder(); + + String paramName; + String paramValue; + + for (_KeyValuePair<String, String> pair : params) { + paramName = pair.getKey(); + paramValue = pair.getValue(); + + if (!usedParams.contains(paramName)) { + queryStringBuilder + .append((includeQueryStringDelimiter && queryStringBuilder.length() == 0) ? "?" : "&"); + + try { + queryStringBuilder.append(UriUtils.encodeQueryParam(paramName, encoding)); + + if (paramValue != null) { + queryStringBuilder.append('='); + queryStringBuilder.append(UriUtils.encodeQueryParam(paramValue, encoding)); + } + } catch (UnsupportedEncodingException e) { + CallableUtils.newGenericExecuteException("Cannot encode query parameter. " + e, this); + } + } + } + + return queryStringBuilder.toString(); + } + + private enum UrlType { + CONTEXT_RELATIVE, RELATIVE, ABSOLUTE + } }