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

com.groupbyinc.util.UrlBeautifier Maven / Gradle / Ivy

There is a newer version: 3.2.0
Show newest version
package com.groupbyinc.util;

import com.groupbyinc.api.Query;
import com.groupbyinc.api.model.Navigation;
import com.groupbyinc.api.model.Refinement;
import com.groupbyinc.api.model.refinement.RefinementRange;
import com.groupbyinc.api.model.refinement.RefinementValue;
import com.groupbyinc.api.parser.ParserException;
import com.groupbyinc.common.apache.commons.collections4.MapUtils;
import com.groupbyinc.common.apache.commons.lang3.ArrayUtils;
import com.groupbyinc.common.apache.commons.lang3.StringUtils;
import com.groupbyinc.common.apache.http.client.utils.URIBuilder;
import com.groupbyinc.injector.StaticInjector;
import com.groupbyinc.injector.StaticInjectorFactory;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static java.util.Arrays.asList;

/**
 * @author will
 */
public class UrlBeautifier {

  public static final String PARAM_REPLACEMENT = "z";
  public static final String SEARCH_NAVIGATION_NAME = "search";
  protected static final StaticInjector> INJECTOR = new StaticInjectorFactory>().create();
  private static final String REFINEMENTS_PARAM_DEFAULT = "refinements";
  private static final String ID = "id";
  private static final Pattern idPattern = Pattern.compile("(?:\\A|.*&)id=([^&]*).*");

  public static class UrlBeautificationException extends Exception {

    public UrlBeautificationException(String message) {
      super(message);
    }

    public UrlBeautificationException(String message, Throwable cause) {
      super(message, cause);
    }
  }

  static {
    INJECTOR.set(new HashMap());
  }

  private final Navigation SEARCH_NAVIGATION = new Navigation().setDisplayName("");
  private List replacementRules = new ArrayList();
  private LinkedHashMap tokenToName = new LinkedHashMap();
  private LinkedHashMap nameToToken = new LinkedHashMap();
  private List remainingMappings = new ArrayList();
  private String refinementsQueryParameterName = REFINEMENTS_PARAM_DEFAULT;
  private String append = null;

  private UrlBeautifier() {
  }

  /**
   * 
   * Create a UrlBeautifier and store it for the lifetime of this JVM under the name specified.
   * 
   *
   * @param name The handle back to this UrlBeautifier
   */
  public static void createUrlBeautifier(String name) {
    getUrlBeautifiers().put(name, new UrlBeautifier());
  }

  /**
   * 
   * Get a map of UrlBeautifiers keyed by name.
   * 
   *
   * @return
   */
  public static Map getUrlBeautifiers() {
    return INJECTOR.get();
  }

  /**
   * 
   * Convert a search term and a list of refinements into a beautified URL.
   * Each refinement that has a mapping will be turned into a path segment.
   * If a mapping has been created for search, the search term will also be
   * placed into a URL path segment.
   * 
   *
   * @deprecated Use {@link #toUrl(String, Map)} ()}
   * @param searchString
   *         The current search state.
   * @param existingRefinements
   *         The current refinement state
   *
   * @return
   *
   * @throws UrlBeautifier.UrlBeautificationException
   */
  @Deprecated
  public String toUrl(String searchString, String existingRefinements) throws UrlBeautifier.UrlBeautificationException {
    StringBuilder pathSegmentLookup = new StringBuilder("/");
    Query query = createQuery();
    if (StringUtils.isNotBlank(searchString)) {
      query.setQuery(searchString);
    }
    URIBuilder uri = new URIBuilder();
    uri.setPath("");
    query.addRefinementsByString(existingRefinements);
    Map navigations = getDistinctRefinements(query);
    addRefinements(query.getQuery(), navigations, pathSegmentLookup, uri);
    addReferenceBlock(pathSegmentLookup, uri);
    addAppend(uri);
    addUnmappedRefinements(navigations, uri);
    String uriString = uri.toString();
    return uriString.startsWith("null") ? uriString.substring(4) : uriString;
  }

  protected Query createQuery() {
    return new Query();
  }

  private Map getDistinctRefinements(Query query) {
    Map navigations = query.getNavigations();
    for (Navigation n : navigations.values()) {
      Set names = new HashSet();
      Iterator iterator = n.getRefinements().iterator();
      while (iterator.hasNext()) {
        Refinement refinement = iterator.next();
        String name = n.getName() + refinement.toTildeString();
        if (!names.contains(name)) {
          names.add(name);
        } else {
          iterator.remove();
        }
      }
    }
    return navigations;
  }

  private void addRefinements(
      String searchString, Map navigations, StringBuilder pathSegmentLookup, URIBuilder uri) throws UrlBeautifier.UrlBeautificationException {
    int indexOffSet = StringUtils.length(uri.getPath()) + 1;
    List replacements = new ArrayList();

    String tempSearchString = searchString;

    for (Navigation m : remainingMappings) {
      if (SEARCH_NAVIGATION.equals(m) && StringUtils.isNotBlank(tempSearchString)) {
        tempSearchString = applyReplacementRule(m, tempSearchString, indexOffSet, replacements);
        indexOffSet += tempSearchString.length() + 1;
        addSearchString(tempSearchString, pathSegmentLookup, uri);
        continue;
      }
      Navigation n = navigations.get(m.getName());
      if (n != null) {
        Iterator ri = n.getRefinements().iterator();
        while (ri.hasNext()) {
          Refinement r = ri.next();
          switch (r.getType()) {
            case Value:
              pathSegmentLookup.append(getToken(n.getName()));
              RefinementValue rv = (RefinementValue) r;
              rv.setValue(applyReplacementRule(n, rv.getValue(), indexOffSet, replacements));
              String encodedRefValue = "/" + UrlEncoder.encode(rv.getValue());
              indexOffSet += rv.getValue().length() + 1;
              uri.setPath(uri.getPath() + encodedRefValue);
              ri.remove();
              break;
            case Range:
              throw new UrlBeautifier.UrlBeautificationException("You should not map ranges into URLs.");
            default:
              break;
          }
        }
        if (n.getRefinements().isEmpty()) {
          navigations.remove(n.getName());
        }
      }
    }
    if (!replacements.isEmpty()) {
      uri.addParameter(PARAM_REPLACEMENT, UrlReplacement.buildQueryString(replacements));
    }
  }

  private void addReferenceBlock(StringBuilder reference, URIBuilder uri) {
    if (reference.length() > 1) {
      uri.setPath(uri.getPath() + reference.toString());
    }
  }

  private void addAppend(URIBuilder uri) {
    if (StringUtils.isNotBlank(append)) {
      uri.setPath(uri.getPath() + append);
    }
  }

  private void addUnmappedRefinements(Map navigations, URIBuilder uri) {
    if (MapUtils.isNotEmpty(navigations)) {
      Query query = createQuery();
      Map distinctRefinements = getDistinctRefinements(query);
      for (Map.Entry entry : navigations.entrySet()) {
        Navigation n = distinctRefinements.get(entry.getKey());
        if (n == null) {
          distinctRefinements.put(entry.getKey(), entry.getValue());
        } else {
          n.getRefinements().addAll(entry.getValue().getRefinements());
        }
      }
      String refinements = query.getRefinementString();
      if (StringUtils.isNotBlank(refinements)) {
        uri.addParameter(refinementsQueryParameterName, query.getRefinementString());
      }
    }
  }

  private String applyReplacementRule(
      Navigation pNavigation, String pValue, int pIndexOffSet, List pReplacements) {
    StringBuilder urlBuilder = new StringBuilder(pValue);
    for (UrlReplacementRule replacementRule : replacementRules) {
      replacementRule.apply(urlBuilder, pIndexOffSet, pNavigation.getName(), pReplacements);
    }
    return urlBuilder.toString();
  }

  private void addSearchString(String searchString, StringBuilder reference, URIBuilder pUri) {
    if (StringUtils.isNotBlank(searchString)) {
      pUri.setPath(pUri.getPath() + "/" + UrlEncoder.encode(searchString));
      reference.append(SEARCH_NAVIGATION.getDisplayName());
    }
  }

  private String getToken(String name) {
    Navigation mapping = nameToToken.get(name);
    return mapping == null ? null : mapping.getDisplayName();
  }

  /**
   * 
   * Convert a search term and a list of refinements into a beautified URL.
   * Each refinement that has a mapping will be turned into a path segment.
   * If a mapping has been created for search, the search term will also be
   * placed into a URL path segment.
   * 
   *
   * @param searchString
   *         The current search state.
   * @param navigations
   *         The current refinement state
   *
   * @return
   *
   * @throws UrlBeautifier.UrlBeautificationException
   */
  public String toUrl(String searchString, Map navigations) throws UrlBeautifier.UrlBeautificationException {
    StringBuilder pathSegmentLookup = new StringBuilder("/");
    Query query = createQuery();
    if (StringUtils.isNotBlank(searchString)) {
      query.setQuery(searchString);
    }
    URIBuilder uri = new URIBuilder();
    uri.setPath("");
    if (MapUtils.isNotEmpty(navigations)) {
      for (Navigation n : navigations.values()) {
        for (Refinement r : n.getRefinements()) {
          if (r instanceof RefinementRange) {
            RefinementRange rr = (RefinementRange) r;
            query.addRangeRefinement(n.getName(), rr.getLow(), rr.getHigh());
          } else {
            query.addValueRefinement(n.getName(), ((RefinementValue) r).getValue());
          }
        }
      }
    }
    Map groupedRefinements = getDistinctRefinements(query);
    addRefinements(query.getQuery(), groupedRefinements, pathSegmentLookup, uri);
    addReferenceBlock(pathSegmentLookup, uri);
    addAppend(uri);
    addUnmappedRefinements(groupedRefinements, uri);
    String uriString = uri.toString();
    return uriString.startsWith("null") ? uriString.substring(4) : uriString;
  }

  /**
   * 
   * Convert a URI into a query object. Mappings will be converted to the
   * correct search and refinement state.
   * 
   *
   * @param uri
   *         The URI to parse into a query object
   *
   * @return
   *
   * @throws UrlBeautifier.UrlBeautificationException
   */
  public Query fromUrl(String uri) throws UrlBeautifier.UrlBeautificationException {
    Query query = fromUrl(uri, null);
    if (query == null) {
      throw new IllegalStateException("URL reference block is invalid, could not convert to query");
    }
    return query;
  }

  /**
   * 
   * Convert a URI into a query object. Mappings will be converted to the
   * correct search and refinement state.
   * 
   *
   * @param url
   *         The URI to parse into a query object
   * @param defaultQuery
   *         The default query to use if this URL does not correctly parse.
   *
   * @return
   *
   * @throws UrlBeautifier.UrlBeautificationException
   */
  public Query fromUrl(String url, Query defaultQuery) throws UrlBeautifier.UrlBeautificationException {
    URI uri;
    try {
      uri = new URI(url);
    } catch (URISyntaxException e) {
      throw new UrlBeautifier.UrlBeautificationException("Unable to parse url", e);
    }
    String urlQueryString = uri.getQuery();
    if (StringUtils.isNotBlank(urlQueryString) && idPattern.matcher(urlQueryString).matches()) {
      Matcher m = idPattern.matcher(urlQueryString);
      m.find();
      return createQuery().addValueRefinement(ID, m.group(1));
    } else {
      Query query = createQuery();
      String replacementUrlQueryString = getReplacementQuery(uri.getRawQuery());
      List pathSegments = new ArrayList();
      String uriPath = uri.getPath();
      if (StringUtils.isNotBlank(append) && uriPath.endsWith(append)) {
        uriPath = uriPath.substring(0, uriPath.length() - append.length());
      }
      pathSegments.addAll(asList(uriPath.split("/")));
      String pathSegmentLookup = lastSegment(pathSegments);

      if (pathSegments.size() > pathSegmentLookup.length()) {
        removeUnusedPathSegments(pathSegments, pathSegmentLookup);
      } else if (pathSegments.size() < pathSegmentLookup.length()) {
        return defaultQuery;
      }
      try {
        pathSegments = applyReplacementToPathSegment(pathSegments, UrlReplacement.parseQueryString(replacementUrlQueryString));
      } catch (ParserException e) {
        throw new UrlBeautifier.UrlBeautificationException("Replacement Query is malformed, returning default query", e);
      }
      while (pathSegments.size() > 0) {
        addRefinement(pathSegments, query, pathSegmentLookup);
      }
      if (StringUtils.isNotBlank(urlQueryString)) {
        String[] queryParams = urlQueryString.split("\\&");
        if (ArrayUtils.isNotEmpty(queryParams)) {
          for (String keyValue : queryParams) {
            if (keyValue.startsWith(refinementsQueryParameterName + "=")) {
              String v = keyValue.substring(refinementsQueryParameterName.length());
              query.addRefinementsByString(v);
              break;
            }
          }
        }
      }
      return query;
    }
  }

  private String getReplacementQuery(String pQuery) {
    if (StringUtils.isNotBlank(pQuery)) {
      for (String token : pQuery.split("&")) {
        if (token.startsWith(PARAM_REPLACEMENT + "=")) {
          return UrlEncoder.decode(token.substring(2));
        }
      }
    }
    return "";
  }

  private String lastSegment(List pathSegments) {
    return pathSegments.remove(pathSegments.size() - 1);
  }

  private void removeUnusedPathSegments(List pathSegments, String pathSegmentLookup) {
    while (pathSegments.size() > pathSegmentLookup.length()) {
      pathSegments.remove(0);
    }
  }

  private List applyReplacementToPathSegment(List pPathSegments, List pReplacements) {
    if (pPathSegments.isEmpty()) {
      return pPathSegments;
    }
    List replacedPathSegments = new ArrayList(pPathSegments.size());
    int indexOffSet = 1;
    for (String pathSegment : pPathSegments) {
      StringBuilder decodedPathSegment = new StringBuilder(UrlEncoder.decode(pathSegment));
      for (UrlReplacement replacement : pReplacements) {
        replacement.apply(decodedPathSegment, indexOffSet);
      }
      replacedPathSegments.add(decodedPathSegment.toString());
      indexOffSet += decodedPathSegment.length() + 1;
    }
    return replacedPathSegments;
  }

  private void addRefinement(List pathSegments, Query query, String referenceBlock) {
    String token = String.valueOf(referenceBlock.charAt(referenceBlock.length() - pathSegments.size()));
    if (token.equals(SEARCH_NAVIGATION.getDisplayName())) {
      query.setQuery(pathSegments.remove(0));
    } else if (getFieldName(token) != null) {
      query.addValueRefinement(getFieldName(token), pathSegments.remove(0));
    } else {
      pathSegments.remove(0);
    }
  }

  private String getFieldName(String token) {
    Navigation mapping = tokenToName.get(token);
    return mapping == null ? null : mapping.getName();
  }

  /**
   * 
   * Set the mapping from a search term to a path segment.
   * Note: you cannot use vowels for mapping tokens to prevent dictionary word creation.
   * The order in which this method is called determines where in the URL the search term will show up.
   * 
   *
   * @param pToken
   *         The single letter to represent search in the lookup.
   */
  public void setSearchMapping(char pToken) {
    SEARCH_NAVIGATION.setName(SEARCH_NAVIGATION_NAME);
    SEARCH_NAVIGATION.setDisplayName(String.valueOf(pToken));
    addMapping(SEARCH_NAVIGATION);
  }

  private void addMapping(Navigation mapping) {
    String name = mapping.getName();
    String token = mapping.getDisplayName();
    if (token.length() != 1 || StringUtils.isBlank(token)) {
      throw new IllegalStateException("Token length must be one");
    }
    if (token.matches("[aoeuiAOEUIyY]")) {
      throw new IllegalStateException("Vowels are not allowed to avoid Dictionary words appearing");
    }
    if (tokenToName.containsKey(token)) {
      throw new IllegalStateException("This token: " + token + " is already mapped to: " + tokenToName.get(token).getName());
    }
    tokenToName.put(token, mapping);
    nameToToken.put(name, mapping);
    remainingMappings.add(mapping);
  }

  /**
   * 
   * Set up a mapping for a refinement.
   * Note: you cannot use vowels for mapping tokens to prevent dictionary word creation.
   * The order in which this method is called determines where in the URL the refinements will show up.
   * 
   *
   * @param token
   *         The single letter to represent this refinement in the lookup.
   * @param name
   *         The name of the navigation that will be mapped using this
   *         token.
   */
  public void addRefinementMapping(char token, String name) {
    Navigation mapping = new Navigation();
    setValues(mapping, name, String.valueOf(token));
    addMapping(mapping);
  }

  private void setValues(Navigation mapping, String name, String token) {
    mapping.setName(name);
    mapping.setDisplayName(token);
  }

  /**
   * 
   * Clean up all mappings.
   * 
   */
  public void clearSavedFields() {
    append = null;
    tokenToName = new LinkedHashMap();
    nameToToken = new LinkedHashMap();
    remainingMappings = new ArrayList();
  }

  /**
   * 
   * Return the current appended URL segment
   * 
   *
   * @return
   */
  public String getAppend() {
    return append;
  }

  /**
   * 
   * Quite often URLs need to end with specific extensions to map to the correct controller in the backend.
   * Here you can set this value.
   * For example:
   * /index.html
   * 
   *
   * @param append
   *         The value to append to each beautified URL.
   */
  public void setAppend(String append) {
    this.append = append;
  }

  /**
   * @return The name with which non-mapped refinements will be mapped into
   * the URL query string.
   */
  public String getRefinementsQueryParameterName() {
    return refinementsQueryParameterName;
  }

  /**
   * 
   * Sets the name of the query parameter with which non-mapped refinements will show up in the query string.
   * This includes ranges which are never mapped to beautified URLs.
   * 
   *
   * @param refinementsQueryParameterName
   *         The name of the query parameter to use.
   */
  public void setRefinementsQueryParameterName(String refinementsQueryParameterName) {
    this.refinementsQueryParameterName = refinementsQueryParameterName;
  }

  /**
   * 
   * Adds a new replacement rule that will be applied to the search term and mapped refinements. The original
   * search term and refinements will be put back into the query object.
   * If replacement is null the target character will be removed.
   * Note: Replacements that are chained may still contain the original target character.
   * For example:
   * addReplacementRule('x','y');
   * addReplacementRule('z','x');
   * The result of this may contain x's in the final result.
   * www.example.com/xyz will become www.example.com/yyz after the first replacement and www.example.com/yyx
   * after the second replacement.
   * 
   *
   * @param target
   *         The char values to be replaced
   * @param replacement
   *         The replacement char value
   */
  public void addReplacementRule(char target, Character replacement) {
    addReplacementRule(target, replacement, null);
  }

  /**
   * 
   * Adds a new replacement rule that will only be applied to the specified refinement. The original
   * search term and refinements will be put back into the query object.
   * If replacement is null the target character will be removed.
   * Note: Replacements that are chained may still contain the original target character.
   * For example:
   * addReplacementRule('x', 'y', "brand");
   * addReplacementRule('z', 'x', "brand");
   * The result of this may contain x's in the final result.
   * www.example.com/xyz/b will become www.example.com/yyz/b after the first replacement and www.example.com/yyx/b
   * after the second replacement.
   * 
   *
   * @param target
   *         The char values to be replaced
   * @param replacement
   *         The replacement char value
   * @param refinementName
   *         The name of the refinement that this replacement should be applied to.
   */
  public void addReplacementRule(char target, Character replacement, String refinementName) {
    if (!((Character) target).equals(replacement)) {
      replacementRules.add(new UrlReplacementRule(target, replacement, refinementName));
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy