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

org.jclouds.http.Uris Maven / Gradle / Ivy

/**
 * Licensed to jclouds, Inc. (jclouds) under one or more
 * contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  jclouds licenses this file
 * to you 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.jclouds.http;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Strings.emptyToNull;
import static com.google.common.collect.Multimaps.forMap;
import static org.jclouds.http.utils.Queries.buildQueryLine;
import static org.jclouds.http.utils.Queries.encodeQueryLine;
import static org.jclouds.http.utils.Queries.queryParser;
import static org.jclouds.util.Strings2.urlDecode;
import static org.jclouds.util.Strings2.urlEncode;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;

import org.jclouds.javax.annotation.Nullable;

import com.google.common.annotations.Beta;
import com.google.common.base.Function;
import com.google.common.collect.ForwardingMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;

/**
 * Functions on {@code String}s and {@link URI}s. Strings can be level 1 RFC6570 form.
 * 
 * ex.
 * 
 * 
 *  https://api.github.com/repos/{user}
 * 
* *

Reminder

* * Unresolved RFC6570 templates are not supported by * {@link URI#create(String)} and result in an {@link IllegalArgumentException}. * *

Limitations

* * In order to reduce complexity not needed in jclouds, this doesn't support {@link URI#getUserInfo()}, * {@link URI#getFragment()}, or {@code matrix} params. Matrix params can be achieved via adding {@code ;} refs in the * http path directly. Moreover, since jclouds only uses level 1 templates, this doesn't support the additional forms * noted in the RFC. * * @since 1.6 * * @author Adrian Cole * */ @Beta public final class Uris { /** * @param template * URI string that can be in level 1 RFC6570 form. */ public static UriBuilder uriBuilder(CharSequence template) { return new UriBuilder(template); } /** * @param in * uri */ public static UriBuilder uriBuilder(URI uri) { return new UriBuilder(uri); } /** * Mutable URI builder that can be in level 1 RFC6570 template form. * * ex. * *
    *  https://api.github.com/repos/{user}
    * 
* */ public static final class UriBuilder { // colon for urns, semicolon & equals for matrix params private Iterable skipPathEncoding = Lists.charactersOf("/:;="); private String scheme; private String host; private Integer port; private String path; private Multimap query = DecodingMultimap.create(); /** * override default of {@code / : ; =} * @param scheme * scheme to set or replace */ public UriBuilder skipPathEncoding(Iterable skipPathEncoding) { this.skipPathEncoding = ImmutableSet.copyOf(checkNotNull(skipPathEncoding, "skipPathEncoding")); return this; } /** * @param scheme * scheme to set or replace */ public UriBuilder scheme(String scheme) { this.scheme = checkNotNull(scheme, "scheme"); return this; } /** * @param host * host to set or replace * @return replaced value */ public UriBuilder host(String host) { this.host = checkNotNull(host, "host"); return this; } public UriBuilder path(@Nullable String path) { path = emptyToNull(path); if (path == null) this.path = null; else this.path = prefixIfNeeded(urlDecode(path)); return this; } public UriBuilder appendPath(String path) { path = urlDecode(checkNotNull(path, "path")); if (this.path == null) { path(path); } else { path(slash(this.path, path)); } return this; } public UriBuilder query(@Nullable String queryLine) { if (query == null) return clearQuery(); return query(queryParser().apply(queryLine)); } public UriBuilder clearQuery() { query.clear(); return this; } public UriBuilder query(Multimap parameters) { checkNotNull(parameters, "parameters"); query.clear(); query.putAll(parameters); return this; } public UriBuilder addQuery(String name, Iterable values) { query.putAll(checkNotNull(name, "name"), checkNotNull(values, "values of %s", name)); return this; } public UriBuilder addQuery(String name, String... values) { return addQuery(name, Arrays.asList(checkNotNull(values, "values of %s", name))); } public UriBuilder addQuery(Multimap parameters) { query.putAll(checkNotNull(parameters, "parameters")); return this; } public UriBuilder replaceQuery(String name, Iterable values) { query.replaceValues(checkNotNull(name, "name"), checkNotNull(values, "values of %s", name)); return this; } public UriBuilder replaceQuery(String name, String... values) { return replaceQuery(name, Arrays.asList(checkNotNull(values, "values of %s", name))); } public UriBuilder replaceQuery(Map parameters) { return replaceQuery(forMap(parameters)); } public UriBuilder replaceQuery(Multimap parameters) { for (String key : checkNotNull(parameters, "parameters").keySet()) replaceQuery(key, parameters.get(key)); return this; } /** * RFC6570 templates have variables defined in curly braces. * Curly brace characters are unparsable via {@link URI#create} and result in an {@link IllegalArgumentException}. * * This implementation temporarily replaces curly braces with double parenthesis so that it can reuse * {@link URI#create}. * * @param uri * template which may have template parameters inside */ private UriBuilder(CharSequence uri) { this(URI.create(escapeSpecialChars(checkNotNull(uri, "uri")))); } private static String escapeSpecialChars(CharSequence uri) { // skip encoding if there's no valid variables set. ex. {a} is the left valid if (uri.length() < 3) return uri.toString(); // duplicates memory even if there are no special characters, however only requires a single scan. StringBuilder builder = new StringBuilder(); for (char c : Lists.charactersOf(uri)) { switch (c) { case '{': builder.append("(("); break; case '}': builder.append("))"); break; default: builder.append(c); } } return builder.toString(); } private static String unescapeSpecialChars(CharSequence uri) { if (uri.length() < 5) // skip encoding if there's no valid variables set. ex. ((a)) is the left valid return uri.toString(); char last = uri.charAt(0);// duplicates even if there are no special characters, but only requires 1 scan StringBuilder builder = new StringBuilder(); for (char c : Lists.charactersOf(uri)) { switch (c) { case '(': if (last == '(') { builder.setCharAt(builder.length() - 1, '{'); } else { builder.append('('); } break; case ')': if (last == ')') { builder.setCharAt(builder.length() - 1, '}'); } else { builder.append(')'); } break; default: builder.append(c); } last = c; } return builder.toString(); } private UriBuilder(URI uri) { checkNotNull(uri, "uri"); this.scheme = uri.getScheme(); this.host = uri.getHost(); this.port = uri.getPort() == -1 ? null : uri.getPort(); if (uri.getPath() != null) path(unescapeSpecialChars(uri.getPath())); if (uri.getQuery() != null) query(queryParser().apply(unescapeSpecialChars(uri.getQuery()))); } public URI build() { return build(ImmutableMap. of()); } /** * @throws IllegalArgumentException * if there's a problem parsing the URI */ public URI build(Map variables) { try { return new URI(expand(variables)); } catch (URISyntaxException e) { throw new IllegalArgumentException(e); } } private String expand(Map variables) { StringBuilder b = new StringBuilder(); if (scheme != null) b.append(scheme).append("://"); if (host != null) b.append(UriTemplates.expand(host, variables)); if (port != null) b.append(':').append(port); if (path != null) b.append(urlEncode(UriTemplates.expand(path, variables), skipPathEncoding)); if (query.size() > 0) b.append('?').append(encodeQueryLine(query)); return b.toString(); } /** * returns template expression without url encoding */ @Override public String toString() { StringBuilder b = new StringBuilder(); if (scheme != null) b.append(scheme).append("://"); if (host != null) b.append(host); if (port != null) b.append(':').append(port); if (path != null) b.append(path); if (query.size() > 0) b.append('?').append(buildQueryLine(query)); return b.toString(); } } private static String slash(CharSequence left, CharSequence right) { return delimit(left, right, '/'); } private static String delimit(CharSequence left, CharSequence right, char token) { if (left.length() == 0) return right.toString(); if (right.length() == 0) return left.toString(); StringBuilder builder = new StringBuilder(left); if (lastChar(left) == token) { if (firstChar(right) == token) // left/ + /right return builder.append(right.subSequence(1, right.length())).toString(); return builder.append(right).toString(); // left/ + right } else if (firstChar(right) == token) { return builder.append(right).toString(); // left + /right } // left + / + right return new StringBuilder(left).append(token).append(right).toString(); } public static boolean lastCharIsToken(CharSequence left, char token) { return lastChar(left) == token; } public static char lastChar(CharSequence in) { return in.charAt(in.length() - 1); } public static char firstChar(CharSequence in) { return in.charAt(0); } public static boolean isToken(CharSequence right, char token) { return right.length() == 1 && right.charAt(0) == token; } private static String prefixIfNeeded(String in) { if (in != null && in.charAt(0) != '/') return new StringBuilder().append('/').append(in).toString(); return in; } /** * Mutable and permits null values. Url decodes all mutations except {@link Multimap#putAll(Multimap)} * * @author Adrian Cole * */ static final class DecodingMultimap extends ForwardingMultimap { private static Multimap create() { return new DecodingMultimap(); } private final Multimap delegate = LinkedHashMultimap.create(); private final Function urlDecoder = new Function() { public Object apply(Object in) { return urlDecode(in); } }; @Override public boolean put(String key, Object value) { return super.put(urlDecode(key), urlDecode(value)); } @Override public boolean putAll(String key, Iterable values) { return super.putAll(urlDecode(key), Iterables.transform(values, urlDecoder)); } @Override public boolean putAll(Multimap multimap) { return super.putAll(multimap); } @Override public Collection replaceValues(String key, Iterable values) { return super.replaceValues(urlDecode(key), Iterables.transform(values, urlDecoder)); } @Override protected Multimap delegate() { return delegate; } } }