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

com.gwtplatform.mvp.shared.proxy.ParameterTokenFormatter Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2011 ArcBees Inc.
 *
 * 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 com.gwtplatform.mvp.shared.proxy;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import javax.inject.Inject;

import com.gwtplatform.common.shared.UrlUtils;

/**
 * Formats tokens from {@code String} values to {@link PlaceRequest} and {@link PlaceRequest}
 * hierarchies and vice-versa. The default implementation
 * uses:
 * 
    *
  • {@code '/'} to separate {@link PlaceRequest}s in a hierarchy;
  • *
  • {@code ';'} to separate parameters in a {@link PlaceRequest};
  • *
  • {@code '='} to separate the parameter name from its value;
  • *
  • {@code '\'} to escape separators inside parameters names and values in a * {@link PlaceRequest}.
  • *
* These symbols cannot be used in a name token. If one of the separating symbol is encountered in a * parameter or a value it is escaped using the {@code '\'} character by replacing {@code '/'} with * {@code '\0'}, {@code ';'} with {@code '\1'}, {@code '='} with {@code '\2'} and {@code '\'} with * {@code '\3'}. *

* Before decoding a {@link String} URL fragment into a {@link PlaceRequest} or a * {@link PlaceRequest} hierarchy, {@link ParameterTokenFormatter} will first pass the * {@link String} through {@link URL#decodeQueryString(String)} so that if the URL was URL-encoded * by some user agent, like a mail user agent, it is still parsed correctly. *

* For example, {@link ParameterTokenFormatter} would parse any of the following: *

*

 * nameToken1%3Bparam1.1%3Dvalue1.1%3Bparam1.2%3Dvalue1.2%2FnameToken2%2FnameToken3%3Bparam3.1%3Dvalue%03%11
 * nameToken1;param1.1=value1.1;param1.2=value1.2/nameToken2/nameToken3;param3.1=value\03\21
 * 
*

* Into the following hierarchy of {@link PlaceRequest}: *

*

 * {
 *   { "nameToken1", { {"param1.1", "value1.1"}, {"parame1.2","value1.2"} },
 *     "nameToken2", {},
 *     "nameToken3", { {"param3.1", "value/3=1"} } }
 * }
 * 
*

* If you want to use different symbols as separator, use the * {@link #ParameterTokenFormatter(String, String, String)} constructor. */ public class ParameterTokenFormatter implements TokenFormatter { protected static final String DEFAULT_HIERARCHY_SEPARATOR = "/"; protected static final String DEFAULT_PARAM_SEPARATOR = ";"; protected static final String DEFAULT_VALUE_SEPARATOR = "="; // Escaped versions of the above. protected static final char ESCAPE_CHARACTER = '\\'; protected static final String ESCAPED_HIERARCHY_SEPARATOR = "\\0"; protected static final String ESCAPED_PARAM_SEPARATOR = "\\1"; protected static final String ESCAPED_VALUE_SEPARATOR = "\\2"; protected static final String ESCAPED_ESCAPE_CHAR = "\\3"; private final UrlUtils urlUtils; private final String hierarchySeparator; private final String paramSeparator; private final String valueSeparator; /** * Builds a {@link ParameterTokenFormatter} using the default separators and escape character. */ @Inject public ParameterTokenFormatter(UrlUtils urlUtils) { this(urlUtils, DEFAULT_HIERARCHY_SEPARATOR, DEFAULT_PARAM_SEPARATOR, DEFAULT_VALUE_SEPARATOR); } /** * This constructor makes it possible to use custom separators in your token formatter. The * separators must be 1-letter strings, they must all be different from one another, and they * must be encoded when ran through {@link URL#encodeQueryString(String)}). * * @param hierarchySeparator The symbol used to separate {@link PlaceRequest} in a hierarchy. * Must be a 1-character string and can't be {@code %}. * @param paramSeparator The symbol used to separate parameters in a {@link PlaceRequest}. Must * be a 1-character string and can't be {@code %}. * @param valueSeparator The symbol used to separate the parameter name from its value. Must be * a 1-character string and can't be {@code %}. */ public ParameterTokenFormatter(UrlUtils urlUtils, String hierarchySeparator, String paramSeparator, String valueSeparator) { assert hierarchySeparator.length() == 1; assert paramSeparator.length() == 1; assert valueSeparator.length() == 1; assert !hierarchySeparator.equals(paramSeparator); assert !hierarchySeparator.equals(valueSeparator); assert !paramSeparator.equals(valueSeparator); assert !valueSeparator.equals(urlUtils.encodeQueryString(valueSeparator)); assert !hierarchySeparator.equals(urlUtils.encodeQueryString(hierarchySeparator)); assert !paramSeparator.equals(urlUtils.encodeQueryString(paramSeparator)); assert !hierarchySeparator.equals("%"); assert !paramSeparator.equals("%"); assert !valueSeparator.equals("%"); this.urlUtils = urlUtils; this.hierarchySeparator = hierarchySeparator; this.paramSeparator = paramSeparator; this.valueSeparator = valueSeparator; } @Override public String toHistoryToken(List placeRequestHierarchy) throws TokenFormatException { StringBuilder out = new StringBuilder(); for (int i = 0; i < placeRequestHierarchy.size(); ++i) { if (i != 0) { out.append(hierarchySeparator); } out.append(placeTokenToUnescapedString(placeRequestHierarchy.get(i))); } return out.toString(); } @Override public PlaceRequest toPlaceRequest(String placeToken) throws TokenFormatException { return unescapedStringToPlaceRequest(urlUtils.decodeQueryString(placeToken)); } /** * Converts an unescaped string to a place request. To unescape the hash fragment you must run it * through {@link URL#decodeQueryString(String)}. * * @param unescapedPlaceToken The unescaped string to convert to a place request. * @return The place request. * @throws TokenFormatException if there is an error converting. */ private PlaceRequest unescapedStringToPlaceRequest(String unescapedPlaceToken) throws TokenFormatException { int split = unescapedPlaceToken.indexOf(paramSeparator); if (split == 0) { throw new TokenFormatException("Place history token is missing."); } else if (split == -1) { // No parameters. return new PlaceRequest.Builder().nameToken(customUnescape(unescapedPlaceToken)).build(); } else if (split >= 0) { PlaceRequest.Builder reqBuilder = new PlaceRequest.Builder().nameToken(customUnescape(unescapedPlaceToken .substring(0, split))); String paramsChunk = unescapedPlaceToken.substring(split + 1); String[] paramTokens = paramsChunk.split(paramSeparator); for (String paramToken : paramTokens) { if (paramToken.isEmpty()) { throw new TokenFormatException("Bad parameter: Successive parameters require a single '" + paramSeparator + "' between them."); } String[] param = paramToken.split(valueSeparator); if (param.length == 1) { // If there is only one parameter, then we need an '=' at the last position. if (paramToken.charAt(paramToken.length() - 1) != valueSeparator.charAt(0)) { throw new TokenFormatException("Bad parameter: Need exactly one key and one value."); } } else if (param.length == 2) { // If there are two parameters, then there must not be a '=' at the last position. if (paramToken.charAt(paramToken.length() - 1) == valueSeparator.charAt(0)) { throw new TokenFormatException("Bad parameter: Need exactly one key and one value."); } } else { throw new TokenFormatException("Bad parameter: Need exactly one key and one value."); } String key = customUnescape(param[0]); String value = param.length == 2 ? customUnescape(param[1]) : ""; reqBuilder = reqBuilder.with(key, value); } return reqBuilder.build(); } return null; } @Override public List toPlaceRequestHierarchy(String historyToken) throws TokenFormatException { String unescapedHistoryToken = urlUtils.decodeQueryString(historyToken); int split = unescapedHistoryToken.indexOf(hierarchySeparator); List result = new ArrayList(); if (split == -1) { // History token consists of a single place token. result.add(unescapedStringToPlaceRequest(unescapedHistoryToken)); } else { String[] unescapedPlaceTokens = unescapedHistoryToken.split(hierarchySeparator); if (unescapedPlaceTokens.length == 0) { throw new TokenFormatException("Bad parameter: nothing in the history token."); } for (String unescapedPlaceToken : unescapedPlaceTokens) { if (unescapedPlaceToken.isEmpty()) { throw new TokenFormatException("Bad parameter: Successive place tokens require a single '" + hierarchySeparator + "' between them."); } result.add(unescapedStringToPlaceRequest(unescapedPlaceToken)); } } return result; } @Override public String toPlaceToken(PlaceRequest placeRequest) throws TokenFormatException { return placeTokenToUnescapedString(placeRequest); } /** * Converts a place token to an unescaped string. If the name token or the parameters contain any * of the separator symbols, they will be escaped with our custom escaping mechanism. * * @param placeRequest The place request to convert. * @return The unescaped string for the place token corresponding to that place request. * @throws TokenFormatException if there is an error converting. */ private String placeTokenToUnescapedString(PlaceRequest placeRequest) throws TokenFormatException { StringBuilder out = new StringBuilder(); out.append(customEscape(placeRequest.getNameToken())); Set params = placeRequest.getParameterNames(); if (params != null) { for (String name : params) { out.append(paramSeparator).append(customEscape(name)).append(valueSeparator).append( customEscape(placeRequest.getParameter(name, null))); } } return out.toString(); } /** * Use our custom escaping mechanism to escape the provided string. This should be used on the * name token, and the parameter keys and values, before they are attached with the various * separators. The string will also be passed through {@link URL#encodeQueryString}. * Visible for testing. * * @param string The string to escape. * @return The escaped string. */ String customEscape(String string) { StringBuilder builder = new StringBuilder(); int len = string.length(); char hierarchyChar = hierarchySeparator.charAt(0); char paramChar = paramSeparator.charAt(0); char valueChar = valueSeparator.charAt(0); for (int i = 0; i < len; i++) { char ch = string.charAt(i); if (ch == ESCAPE_CHARACTER) { builder.append(ESCAPED_ESCAPE_CHAR); } else if (ch == hierarchyChar) { builder.append(ESCAPED_HIERARCHY_SEPARATOR); } else if (ch == paramChar) { builder.append(ESCAPED_PARAM_SEPARATOR); } else if (ch == valueChar) { builder.append(ESCAPED_VALUE_SEPARATOR); } else { builder.append(ch); } } return urlUtils.encodeQueryString(builder.toString()); } /** * Use our custom escaping mechanism to unescape the provided string. This should be used on the * name token, and the parameter keys and values, after they have been split using the various * separators. The input string is expected to already be sent through * {@link URL#decodeQueryString}. * * @param string The string to unescape, must have passed through {@link URL#decodeQueryString}. * @return The unescaped string. * @throws TokenFormatException if there is an error converting. */ private String customUnescape(String string) throws TokenFormatException { StringBuilder builder = new StringBuilder(); int len = string.length(); char hierarchyNum = ESCAPED_HIERARCHY_SEPARATOR.charAt(1); char paramNum = ESCAPED_PARAM_SEPARATOR.charAt(1); char valueNum = ESCAPED_VALUE_SEPARATOR.charAt(1); char escapeNum = ESCAPED_ESCAPE_CHAR.charAt(1); int i = 0; while (i < len - 1) { char ch = string.charAt(i); if (ch == ESCAPE_CHARACTER) { i++; char ch2 = string.charAt(i); if (ch2 == hierarchyNum) { builder.append(hierarchySeparator); } else if (ch2 == paramNum) { builder.append(paramSeparator); } else if (ch2 == valueNum) { builder.append(valueSeparator); } else if (ch2 == escapeNum) { builder.append(ESCAPE_CHARACTER); } } else { builder.append(ch); } i++; } if (i == len - 1) { char ch = string.charAt(i); if (ch == ESCAPE_CHARACTER) { throw new TokenFormatException("Last character of string being unescaped cannot be '" + ESCAPE_CHARACTER + "'."); } builder.append(ch); } return builder.toString(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy