blob: 1b1f6ea790a47357e81f121625006083d91c7bd0 [file] [log] [blame]
/*
* Copyright 2014 The Kythe Authors. All rights reserved.
*
* 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.google.devtools.kythe.util;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.base.Strings.nullToEmpty;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.escape.Escaper;
import com.google.common.net.PercentEscaper;
import com.google.devtools.kythe.proto.Storage.VName;
import java.io.Serializable;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;
/**
* /{@link String} realization of a {@link VName}.
*
* <p>Specification: //kythe/util/go/kytheuri/kythe-uri-spec.txt
*/
public class KytheURI implements Serializable {
private static final long serialVersionUID = 2726281203803801095L;
public static final String SCHEME_LABEL = "kythe";
public static final String SCHEME = SCHEME_LABEL + ":";
public static final KytheURI EMPTY = new KytheURI();
private static final String SAFE_CHARS = ".-_~";
private static final Escaper PATH_ESCAPER = new PercentEscaper(SAFE_CHARS + "/", false);
private static final Escaper ALL_ESCAPER = new PercentEscaper(SAFE_CHARS, false);
private final VName vName;
/** Construct a new {@link KytheURI}. */
public KytheURI(String signature, String corpus, String root, String path, String language) {
this(
VName.newBuilder()
.setSignature(nullToEmpty(signature))
.setCorpus(nullToEmpty(corpus))
.setRoot(nullToEmpty(root))
.setPath(nullToEmpty(path))
.setLanguage(nullToEmpty(language))
.build());
}
/** Constructs an empty {@link KytheURI}. */
private KytheURI() {
this(VName.getDefaultInstance());
}
/** Unpacks a {@link VName} into a new {@link KytheURI}. */
public KytheURI(VName vName) {
this.vName = vName;
}
/** Returns the {@link KytheURI}'s signature. */
public String getSignature() {
return vName.getSignature();
}
/** Returns the {@link KytheURI}'s corpus name. */
public String getCorpus() {
return vName.getCorpus();
}
/** Returns the {@link KytheURI}'s corpus path. */
public String getPath() {
return vName.getPath();
}
/** Returns the {@link KytheURI}'s corpus root. */
public String getRoot() {
return vName.getRoot();
}
/** Returns the {@link KytheURI}'s language. */
public String getLanguage() {
return vName.getLanguage();
}
/** Returns an equivalent {@link VName}. */
public VName toVName() {
return vName;
}
@Override
public String toString() {
StringBuilder b =
new StringBuilder(
SCHEME.length()
+ "//?=?=?=#".length() // assume all punctuation is necessary
+ getCorpus().length()
+ getPath().length()
+ getRoot().length()
+ getLanguage().length()
+ getSignature().length());
b.append(SCHEME);
if (!vName.getCorpus().isEmpty()) {
b.append("//");
b.append(PATH_ESCAPER.escape(vName.getCorpus()));
}
attr(b, "lang", ALL_ESCAPER.escape(vName.getLanguage()));
attr(b, "path", PATH_ESCAPER.escape(cleanPath(vName.getPath())));
attr(b, "root", PATH_ESCAPER.escape(vName.getRoot()));
if (!vName.getSignature().isEmpty()) {
b.append("#");
b.append(ALL_ESCAPER.escape(vName.getSignature()));
}
return b.toString();
}
@Override
public int hashCode() {
return vName.hashCode();
}
@Override
public boolean equals(Object o) {
return this == o || (o instanceof KytheURI && vName.equals(((KytheURI) o).vName));
}
/** Returns an equivalent Kythe ticket for the given {@link VName}. */
public static String asString(VName vName) {
return new KytheURI(vName).toString();
}
/** Parses the given string to produce a new {@link KytheURI}. */
public static KytheURI parse(String str) {
checkNotNull(str, "str must be non-null");
if (str.isEmpty() || str.equals(SCHEME) || str.equals(SCHEME + "//")) {
return EMPTY;
}
String original = str;
// Check for a scheme label. This may be empty; but if present, it must be
// our expected scheme.
if (str.startsWith(SCHEME)) {
str = str.substring(SCHEME.length());
}
String signature = null;
String corpus = null;
String path = null;
String root = null;
String lang = null;
// Split corpus/attributes from signature.
Iterator<String> parts = Splitter.on('#').split(str).iterator();
String head = parts.next();
if (parts.hasNext()) {
signature = parts.next();
}
checkArgument(!parts.hasNext(), "URI has multiple fragments: %s", original);
// Remove corpus prefix from attributes.
int firstParam = head.indexOf('?');
firstParam = firstParam < 0 ? head.length() : firstParam;
if (head.startsWith("//")) {
corpus = head.substring(2, firstParam);
head = head.substring(firstParam);
} else {
checkArgument(firstParam == 0, "invalid URI scheme: %s", original);
}
// If there are any attributes, parse them. We allow valid attributes to
// occur in any order, even if it is not canonical.
if (!head.isEmpty()) {
Map<String, String> params =
Splitter.on('?').withKeyValueSeparator("=").split(head.substring(1));
for (Map.Entry<String, String> e : params.entrySet()) {
switch (e.getKey()) {
case "path":
path = e.getValue();
break;
case "root":
root = e.getValue();
break;
case "lang":
lang = e.getValue();
break;
default:
checkArgument(false, "invalid attribute: %s (value: %s)", e.getKey(), e.getValue());
break;
}
}
}
return new KytheURI(
decode(signature), decode(corpus), decode(root), decode(path), decode(lang));
}
/** Parses the given Kythe ticket string to produce a new {@link VName}. */
public static VName parseVName(String ticket) {
return parse(ticket).toVName();
}
private static String decode(String str) {
if (isNullOrEmpty(str)) {
return null;
}
try {
return URLDecoder.decode(str.replace("+", "%2B"), "UTF-8");
} catch (java.io.UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
/** Returns a new {@link KytheURI.Builder}. */
public static Builder newBuilder() {
return new Builder();
}
public static class Builder {
private final VName.Builder builder = VName.newBuilder();
public Builder setSignature(String signature) {
builder.setSignature(nullToEmpty(signature));
return this;
}
public Builder setCorpus(String corpus) {
builder.setCorpus(nullToEmpty(corpus));
return this;
}
public Builder setPath(String path) {
builder.setPath(nullToEmpty(path));
return this;
}
public Builder setRoot(String root) {
builder.setRoot(nullToEmpty(root));
return this;
}
public Builder setLanguage(String language) {
builder.setLanguage(nullToEmpty(language));
return this;
}
public KytheURI build() {
return new KytheURI(builder.build());
}
}
private static void attr(StringBuilder b, String name, String value) {
if (value.isEmpty()) {
return;
}
b.append("?");
b.append(name);
b.append("=");
b.append(value);
}
/**
* Returns a lexically cleaned path, with repeated slashes and "." path components removed, and
* ".." path components rewound as far as possible without touching the filesystem.
*/
private static String cleanPath(String path) {
ArrayList<String> clean = new ArrayList<>();
for (String part : Splitter.on('/').split(path)) {
if (part.isEmpty() || part.equals(".")) {
continue; // skip empty path components and "here" markers.
} else if (part.equals("..") && !clean.isEmpty()) {
clean.remove(clean.size() - 1);
continue; // back off if possible for "up" (..) markers.
} else {
clean.add(part);
}
}
return Joiner.on('/').join(clean);
}
}