All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.thymeleaf.linkbuilder.StandardLinkBuilder Maven / Gradle / Ivy

Go to download

Modern server-side Java template engine for both web and standalone environments

There is a newer version: 3.1.3.RELEASE
Show newest version
/*
 * =============================================================================
 *
 *   Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org)
 *
 *   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 org.thymeleaf.linkbuilder;

import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.thymeleaf.context.IExpressionContext;
import org.thymeleaf.context.IWebContext;
import org.thymeleaf.exceptions.TemplateProcessingException;
import org.thymeleaf.util.Validate;
import org.unbescape.uri.UriEscape;


/**
 * 

* Standard implementation of {@link ILinkBuilder}. *

*

* This class will build link URLs using (by default) the Java Servlet API when the specified URLs are * context-relative, given the need to obtain the context path and add it to the URL. Also, when an * {@link org.thymeleaf.context.IWebContext} implementation is used as context, URLs will be passed to * the standard {@code HttpSerlvetResponse.encodeURL(...)} method before returning. *

*

* Note however that the Servlet-API specific part of this behaviour is configurable and confined to a set of * {@code protected} methods that can be overwritten by subclasses that want to offer a link building * behaviour very similar to the standard one, but without any dependencies on the Servlet API (e.g. extracting * URL context paths from a framework artifact other than {@code HttpServletRequest}). *

*

* This implementation will only return {@code null} at {@link #buildLink(IExpressionContext, String, Map)} * if the specified {@code base} argument is {@code null}. *

* * @author Daniel Fernández * * @since 3.0.0 */ public class StandardLinkBuilder extends AbstractLinkBuilder { protected enum LinkType { ABSOLUTE, CONTEXT_RELATIVE, SERVER_RELATIVE, BASE_RELATIVE } private static final char URL_TEMPLATE_DELIMITER_PREFIX = '{'; private static final char URL_TEMPLATE_DELIMITER_SUFFIX = '}'; private static final String URL_TEMPLATE_DELIMITER_SEGMENT_PREFIX = "{/"; public StandardLinkBuilder() { super(); } public final String buildLink( final IExpressionContext context, final String base, final Map parameters) { Validate.notNull(context, "Expression context cannot be null"); if (base == null) { return null; } // We need to create a copy that is: 1. defensive, 2. mutable final Map linkParameters = (parameters == null || parameters.size() == 0? null : new LinkedHashMap(parameters)); filterOutJavaScriptLinks(base); final LinkType linkType; if (isLinkBaseAbsolute(base)) { linkType = LinkType.ABSOLUTE; } else if (isLinkBaseContextRelative(base)) { linkType = LinkType.CONTEXT_RELATIVE; } else if (isLinkBaseServerRelative(base)) { linkType = LinkType.SERVER_RELATIVE; } else { linkType = LinkType.BASE_RELATIVE; } /* * Compute URL fragments (selectors after '#') so that they can be output at the end of * the URL, after parameters. */ final int hashPosition = findCharInSequence(base, '#'); /* * Compute whether we might have variable templates (e.g. Spring Path Variables) inside this link base * that we might need to resolve afterwards */ final boolean mightHaveVariableTemplates = findCharInSequence(base, URL_TEMPLATE_DELIMITER_PREFIX) >= 0; /* * Precompute the context path, so that it can be afterwards used for determining if it has to be added to the * URL (in case it is context-relative) or not. * * Note we give subclasses the opportunity to customize the computation of this context path. */ final String contextPath = (linkType == LinkType.CONTEXT_RELATIVE? computeContextPath(context, base, parameters) : null); final boolean contextPathEmpty = contextPath == null || contextPath.length() == 0 || contextPath.equals("/"); /* * SHORTCUT - just before starting to work with StringBuilders, and in the case that we know: 1. That the URL is * absolute, relative or context-relative with no context; 2. That there are no parameters; and * 3. That there are no URL fragments -> then just return the base URL String without further * processing (except HttpServletResponse-encoding if needed, of course...) */ if (contextPathEmpty && linkType != LinkType.SERVER_RELATIVE && (linkParameters == null || linkParameters.size() == 0) && hashPosition < 0 && !mightHaveVariableTemplates) { return processLink(context, base); } /* * Build the StringBuilder that will be used as a base for all URL-related operations from now on: variable * templates, parameters, URL fragments... */ StringBuilder linkBase = new StringBuilder(base); /* * Compute URL fragments (selectors after '#') so that they can be output at the end of * the URL, after parameters. */ String urlFragment = ""; // If hash position == 0 we will not consider it as marking an // URL fragment. if (hashPosition > 0) { // URL fragment String will include the # sign urlFragment = linkBase.substring(hashPosition); linkBase.delete(hashPosition, linkBase.length()); } /* * Replace those variable templates that might appear referenced in the path itself, as for example, Spring * "Path Variables" (e.g. '/something/{variable}/othersomething') */ if (mightHaveVariableTemplates) { linkBase = replaceTemplateParamsInBase(linkBase, linkParameters); } /* * Process parameters (those that have not already been processed as a result of replacing template * parameters in base). */ if (linkParameters != null && linkParameters.size() > 0) { final boolean linkBaseHasQuestionMark = findCharInSequence(linkBase,'?') >= 0; // If there is no '?' in linkBase, we have to replace with first '&' with '?' if (linkBaseHasQuestionMark) { linkBase.append('&'); } else { linkBase.append('?'); } // Build the parameters query. The result will always start with '&' processAllRemainingParametersAsQueryParams(linkBase, linkParameters); } /* * Once parameters have been added (if there are parameters), we can add the URL fragment */ if (urlFragment.length() > 0) { linkBase.append(urlFragment); } /* * If link base is server relative, we will delete now the leading '~' character so that it starts with '/' */ if (linkType == LinkType.SERVER_RELATIVE) { linkBase.delete(0,1); } /* * It's finally a good moment to insert the context path if it is not empty */ if (linkType == LinkType.CONTEXT_RELATIVE && !contextPathEmpty) { // Add the application's context path at the beginning linkBase.insert(0, contextPath); } /* * Return the link, first performing the last processing on it. This will normally perform a standard * HttpServletResponse.encodeUrl(...) operation on it, but will give any subclasses the opportunity to * customize this behaviour (in case, for instance, they don't want to rely on the Java Servlet API). */ return processLink(context, linkBase.toString()); } private static int findCharInSequence(final CharSequence seq, final char character) { int n = seq.length(); while (n-- != 0) { final char c = seq.charAt(n); if (c == character) { return n; } } return -1; } private static void filterOutJavaScriptLinks(final CharSequence linkBase) { if (linkBase.length() >= 11 && Character.toLowerCase(linkBase.charAt(0)) == 'j' && Character.toLowerCase(linkBase.charAt(1)) == 'a' && Character.toLowerCase(linkBase.charAt(2)) == 'v' && Character.toLowerCase(linkBase.charAt(3)) == 'a' && Character.toLowerCase(linkBase.charAt(4)) == 's' && Character.toLowerCase(linkBase.charAt(5)) == 'c' && Character.toLowerCase(linkBase.charAt(6)) == 'r' && Character.toLowerCase(linkBase.charAt(7)) == 'i' && Character.toLowerCase(linkBase.charAt(8)) == 'p' && Character.toLowerCase(linkBase.charAt(9)) == 't' && Character.toLowerCase(linkBase.charAt(10)) == ':') { throw new TemplateProcessingException( "'javascript:' is forbidden in this context. Link expressions cannot " + "contain inlined JavaScript code."); } } private static boolean isLinkBaseAbsolute(final CharSequence linkBase) { final int linkBaseLen = linkBase.length(); if (linkBaseLen < 2) { return false; } final char c0 = linkBase.charAt(0); if (c0 == 'm' || c0 == 'M') { // Let's check for "mailto:" if (linkBase.length() >= 7 && Character.toLowerCase(linkBase.charAt(1)) == 'a' && Character.toLowerCase(linkBase.charAt(2)) == 'i' && Character.toLowerCase(linkBase.charAt(3)) == 'l' && Character.toLowerCase(linkBase.charAt(4)) == 't' && Character.toLowerCase(linkBase.charAt(5)) == 'o' && Character.toLowerCase(linkBase.charAt(6)) == ':') { return true; } } else if (c0 == '/') { return linkBase.charAt(1) == '/'; // It starts with '//' -> true, any other '/x' -> false } for (int i = 0; i < (linkBaseLen - 2); i++) { // Let's try to find the '://' sequence anywhere in the base --> true if (linkBase.charAt(i) == ':' && linkBase.charAt(i + 1) == '/' && linkBase.charAt(i + 2) == '/') { return true; } } return false; } private static boolean isLinkBaseContextRelative(final CharSequence linkBase) { // For this to be true, it should start with '/', but not with '//' if (linkBase.length() == 0 || linkBase.charAt(0) != '/') { return false; } return linkBase.length() == 1 || linkBase.charAt(1) != '/'; } private static boolean isLinkBaseServerRelative(final CharSequence linkBase) { // For this to be true, it should start with '~/' return (linkBase.length() >= 2 && linkBase.charAt(0) == '~' && linkBase.charAt(1) == '/'); } private static StringBuilder replaceTemplateParamsInBase(final StringBuilder linkBase, final Map parameters) { /* * If parameters is null, there's nothing to do */ if (parameters == null) { return linkBase; } /* * Search {templateVar} in linkBase, and replace with value. * Parameters can be multivalued, in which case they will be comma-separated. * Parameter values will be URL-path-encoded. If there is a '?' char, only parameter values before this * char will be URL-path-encoded, whereas parameters after it will be URL-query-encoded. */ final int questionMarkPosition = findCharInSequence(linkBase, '?'); final Set parameterNames = parameters.keySet(); Set alreadyProcessedParameters = null; for (final String parameterName : parameterNames) { // We default to escaping as a path, not a path segment boolean escapeAsPathSegment = false; // We use the text repository in order to avoid the unnecessary creation of too many instances of the same string String template = URL_TEMPLATE_DELIMITER_PREFIX + parameterName + URL_TEMPLATE_DELIMITER_SUFFIX; int templateIndex = linkBase.indexOf(template); // not great, because StringBuilder.indexOf ends up calling template.toCharArray(), but... if (templateIndex < 0) { template = URL_TEMPLATE_DELIMITER_SEGMENT_PREFIX + parameterName + URL_TEMPLATE_DELIMITER_SUFFIX; templateIndex = linkBase.indexOf(template); if (templateIndex < 0) { // This parameter is not one of those used in path variables continue; } // We need to escape this parameter value as a path segment rather than a path escapeAsPathSegment = true; } // Add the parameter name to the set of processed ones so that it is later removed from the parameters object if (alreadyProcessedParameters == null) { alreadyProcessedParameters = new HashSet(parameterNames.size()); } alreadyProcessedParameters.add(parameterName); // Compute the replacement (unescaped!) final Object parameterValue = parameters.get(parameterName); final String templateReplacement = formatParameterValueAsUnescapedVariableTemplate(parameterValue); final int templateReplacementLen = templateReplacement.length(); // We will now use a the StringBuilder itself for replacing all appearances of the variable template in // the link base. Note we do this instead of using String#replace() because String#replace internally uses // pattern matching and is very slow :-( final int templateLen = template.length(); int start = templateIndex; while (start > -1) { // Depending on whether the template appeared before or after the ?, we will apply different escaping final String escapedReplacement = (questionMarkPosition == -1 || start < questionMarkPosition? (escapeAsPathSegment ? UriEscape.escapeUriPathSegment(templateReplacement) : UriEscape.escapeUriPath(templateReplacement)) : UriEscape.escapeUriQueryParam(templateReplacement)); linkBase.replace(start, start + templateLen, escapedReplacement); start = linkBase.indexOf(template, start + templateReplacementLen); } } if (alreadyProcessedParameters != null) { for (final String alreadyProcessedParameter : alreadyProcessedParameters) { parameters.remove(alreadyProcessedParameter); } } return linkBase; } /* * This method will return a String containing all the values for a specific parameter, separated with commas * and suitable therefore to be used as variable template (path variables) replacements */ private static String formatParameterValueAsUnescapedVariableTemplate(final Object parameterValue) { // Get the value if (parameterValue == null) { // If null (= NO_VALUE), empty String return ""; } // If it is not multivalued (e.g. non-List) simply escape and return if (!(parameterValue instanceof List)) { return parameterValue.toString(); } // It is multivalued, so iterate and escape each item (no need to escape the comma separating them, it's an allowed char) final List values = (List)parameterValue; final int valuesLen = values.size(); final StringBuilder strBuilder = new StringBuilder(valuesLen * 16); for (int i = 0; i < valuesLen; i++) { final Object valueItem = values.get(i); if (strBuilder.length() > 0) { strBuilder.append(','); } strBuilder.append(valueItem == null? "" : valueItem.toString()); } return strBuilder.toString(); } private static void processAllRemainingParametersAsQueryParams(final StringBuilder strBuilder, final Map parameters) { final int parameterSize = parameters.size(); if (parameterSize <= 0) { return; } final Set parameterNames = parameters.keySet(); int i = 0; for (final String parameterName : parameterNames) { final Object value = parameters.get(parameterName); if (value == null) { if (i > 0) { strBuilder.append('&'); } strBuilder.append(UriEscape.escapeUriQueryParam(parameterName)); i++; continue; } if (!(value instanceof List)) { if (i > 0) { strBuilder.append('&'); } strBuilder.append(UriEscape.escapeUriQueryParam(parameterName)); strBuilder.append('='); strBuilder.append(UriEscape.escapeUriQueryParam(value.toString())); // we know it's not null i++; continue; } // It is multivalued, so iterate and process each value final List values = (List)value; final int valuesLen = values.size(); for (int j = 0; j < valuesLen; j++) { final Object valueItem = values.get(j); if (i > 0 || j > 0) { strBuilder.append('&'); } strBuilder.append(UriEscape.escapeUriQueryParam(parameterName)); if (valueItem != null) { strBuilder.append('='); strBuilder.append(UriEscape.escapeUriQueryParam(valueItem.toString())); } } i++; } } /** *

* Compute the context path to be applied to URLs that have been determined to be context-relative (and therefore * need a context path to be inserted at their beginning). *

*

* By default, this method will obtain the context path from {@code HttpServletRequest.getContextPath()}, * throwing an exception if {@code context} is not an instance of {@code IWebContext} given context-relative * URLs are (by default) only allowed in web contexts. *

*

* This method can be overridden by any subclasses that want to change this behaviour (e.g. in order to * avoid using the Servlet API for resolving context path or to allow context-relative URLs in non-web * contexts). *

* * @param context the execution context. * @param base the URL base specified. * @param parameters the URL parameters specified. * @return the context path. */ protected String computeContextPath( final IExpressionContext context, final String base, final Map parameters) { if (!(context instanceof IWebContext)) { throw new TemplateProcessingException( "Link base \"" + base + "\" cannot be context relative (/...) unless the context " + "used for executing the engine implements the " + IWebContext.class.getName() + " interface"); } // If it is context-relative, it has to be a web context final HttpServletRequest request = ((IWebContext)context).getRequest(); return request.getContextPath(); } /** *

* Process an already-built URL just before returning it. *

*

* By default, this method will apply the {@code HttpServletResponse.encodeURL(url)} mechanism, as standard * when using the Java Servlet API. Note however that this will only be applied if {@code context} is * an implementation of {@code IWebContext} (i.e. the Servlet API will only be applied in web environments). *

*

* This method can be overridden by any subclasses that want to change this behaviour (e.g. in order to * avoid using the Servlet API). *

* * @param context the execution context. * @param link the already-built URL. * @return the processed URL, ready to be used. */ protected String processLink(final IExpressionContext context, final String link) { if (!(context instanceof IWebContext)) { return link; } final HttpServletResponse response = ((IWebContext)context).getResponse(); return (response != null? response.encodeURL(link) : link); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy