/** * The MIT License (MIT) * * Copyright (c) 2014-2017 Yegor Bugayenko * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package org.takes.misc; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.charset.Charset; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.SortedMap; import java.util.TreeMap; import java.util.regex.Pattern; /** * HTTP URI/HREF. * * <p>The class is immutable and thread-safe. * @author Yegor Bugayenko (yegor256@gmail.com) * @version $Id: 4112c6b0829206b2e72b80adc483dc50fae00d95 $ * @since 0.7 */ @SuppressWarnings ( { "PMD.TooManyMethods", "PMD.OnlyOneConstructorShouldDoInitialization" } ) public final class Href implements CharSequence { /** * Pattern matching trailing slash. */ private static final Pattern TRAILING_SLASH = Pattern.compile("/$"); /** * URI (without the query part). */ private final URI uri; /** * Params. */ private final SortedMap<String, List<String>> params; /** * Ctor. */ public Href() { this("/"); } /** * Ctor. * @param txt Text of the link */ public Href(final CharSequence txt) { this(Href.createUri(txt.toString())); } /** * Ctor. * @param link The link */ private Href(final URI link) { this(Href.removeQuery(link), Href.asMap(link.getRawQuery())); } /** * Ctor. * @param link The link * @param map Map of params */ private Href(final URI link, final SortedMap<String, List<String>> map) { this.uri = link; this.params = map; } @Override public int length() { return this.toString().length(); } @Override public char charAt(final int index) { return this.toString().charAt(index); } @Override public CharSequence subSequence(final int start, final int end) { return this.toString().subSequence(start, end); } @Override public String toString() { final StringBuilder text = new StringBuilder(this.bare()); if (!this.params.isEmpty()) { boolean first = true; for (final Map.Entry<String, List<String>> ent : this.params.entrySet()) { for (final String value : ent.getValue()) { if (first) { text.append('?'); first = false; } else { text.append('&'); } text.append(Href.encode(ent.getKey())); if (!value.isEmpty()) { text.append('=').append(Href.encode(value)); } } } } return text.toString(); } /** * Get path part of the HREF. * @return Path * @since 0.9 */ public String path() { return this.uri.getPath(); } /** * Get URI without params. * @return Bare URI * @since 0.14 */ public String bare() { final StringBuilder text = new StringBuilder(this.uri.toString()); if (this.uri.getPath().isEmpty()) { text.append('/'); } return text.toString(); } /** * Get query param. * @param key Param name * @return Values (could be empty) * @since 0.9 */ public Iterable<String> param(final Object key) { final List<String> values = this.params.get(key.toString()); final Iterable<String> iter; if (values == null) { iter = new VerboseIterable<String>( Collections.<String>emptyList(), String.format( "there are no URI params by name \"%s\" among %d others", key, this.params.size() ) ); } else { iter = new VerboseIterable<String>( values, String.format( "there are only %d URI params by name \"%s\"", values.size(), key ) ); } return iter; } /** * Add this path to the URI. * @param suffix The suffix * @return New HREF */ public Href path(final Object suffix) { return new Href( URI.create( new StringBuilder( Href.TRAILING_SLASH.matcher(this.uri.toString()) .replaceAll("") ) .append('/') .append(Href.encode(suffix.toString())).toString() ), this.params ); } /** * Add this extra param. * @param key Key of the param * @param value The value * @return New HREF */ public Href with(final Object key, final Object value) { final SortedMap<String, List<String>> map = new TreeMap<>(this.params); if (!map.containsKey(key.toString())) { map.put(key.toString(), new LinkedList<String>()); } map.get(key.toString()).add(value.toString()); return new Href(this.uri, map); } /** * Without this query param. * @param key Key of the param * @return New HREF */ public Href without(final Object key) { final SortedMap<String, List<String>> map = new TreeMap<>(this.params); map.remove(key.toString()); return new Href(this.uri, map); } /** * Encode into URL. * @param txt Text * @return Encoded */ private static String encode(final String txt) { try { return URLEncoder.encode( txt, Charset.defaultCharset().name() ); } catch (final UnsupportedEncodingException ex) { throw new IllegalStateException(ex); } } /** * Decode from URL. * @param txt Text * @return Decoded */ private static String decode(final String txt) { try { return URLDecoder.decode( txt, Charset.defaultCharset().name() ); } catch (final UnsupportedEncodingException ex) { throw new IllegalStateException(ex); } } /** * Parses the specified content to create the corresponding {@code URI} * instance. In case of an {@code URISyntaxException}, it will automatically * encode the character that causes the issue then it will try again * if it is possible otherwise an {@code IllegalArgumentException} will * be thrown. * @param txt The content to parse * @return The {@code URI} corresponding to the specified content. * @throws IllegalArgumentException in case the content could not be parsed * @throws IllegalStateException in case an invalid character could not be * encoded properly. */ private static URI createUri(final String txt) { URI result; try { result = new URI(txt); } catch (final URISyntaxException ex) { final int index = ex.getIndex(); if (index == -1) { throw new IllegalArgumentException(ex.getMessage(), ex); } final StringBuilder value = new StringBuilder(txt); value.replace( index, index + 1, Href.encode(value.substring(index, index + 1)) ); result = Href.createUri(value.toString()); } return result; } /** * Convert the provided query into a Map. * @param query The query to parse. * @return A map containing all the query arguments and their values. */ @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") private static SortedMap<String, List<String>> asMap(final String query) { final SortedMap<String, List<String>> params = new TreeMap<>(); if (query != null) { for (final String pair : query.split("&")) { final String[] parts = pair.split("=", 2); final String key = Href.decode(parts[0]); final String value; if (parts.length > 1) { value = Href.decode(parts[1]); } else { value = ""; } if (!params.containsKey(key)) { params.put(key, new LinkedList<String>()); } params.get(key).add(value); } } return params; } /** * Remove the query part from the provided URI and return the resulting URI. * @param link The link from which the query needs to be removed. * @return The URI corresponding to the same provided URI but without the * query part. */ private static URI removeQuery(final URI link) { final String query = link.getRawQuery(); final URI uri; if (query == null) { uri = link; } else { final String href = link.toString(); uri = URI.create( href.substring(0, href.length() - query.length() - 1) ); } return uri; } }