| // Copyright 2015 The Vanadium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package io.v.impl.google.naming; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.util.Arrays; |
| import java.util.Iterator; |
| import java.util.List; |
| |
| import com.google.common.base.CharMatcher; |
| import com.google.common.base.Joiner; |
| import com.google.common.base.Splitter; |
| import com.google.common.collect.ImmutableList; |
| |
| /** |
| * Utilities for dealing with Vanadium names. |
| */ |
| public class NamingUtil { |
| /** |
| * Takes an object name and returns the server address and the name relative to the server. |
| * <p> |
| * The name parameter may be a rooted name or a relative name; an empty string |
| * address is returned for the latter case. |
| * <p> |
| * The returned address may be in endpoint format or {@code host:port} format. |
| * <p> |
| * The returned list is guaranteed to contain exactly two entries: the server address and |
| * the name relative to the server. |
| * |
| * @param name name from which the server address and relative name are extracted |
| * @return a list containing exactly two entries: the server address and |
| * the name relative to the server |
| */ |
| public static List<String> splitAddressName(String name) { |
| name = clean(name); |
| if (!isRooted(name)) { |
| return ImmutableList.of("", name); |
| } |
| name = name.substring(1, name.length()); // trim the beginning "/" |
| if (name.isEmpty()) { |
| return ImmutableList.of("", ""); |
| } |
| // Could have used regular expressions, but that makes this function |
| // 10x slower as per the benchmark. |
| if (name.startsWith("@")) { // <endpoint>/<suffix> |
| List<String> parts = splitInTwo(name, "@@/"); |
| String addr = parts.get(0), suffix = parts.get(1); |
| if (!suffix.isEmpty()) { // The trailing "@@" was stripped, restore it |
| addr += "@@"; |
| } |
| return ImmutableList.of(addr, suffix); |
| } |
| if (name.startsWith("(")) { // (blessing)@host:[port]/suffix |
| String tmp = splitInTwo(name, ")@").get(1); |
| String suffix = splitInTwo(tmp, "/").get(1); |
| String addr = trimSuffix(name, "/" + suffix); |
| if (addr.endsWith("/" + suffix)) { |
| addr = addr.substring(0, addr.length() - suffix.length() - 1); |
| } |
| return ImmutableList.of(addr, suffix); |
| } |
| // host:[port]/suffix |
| List<String> parts = splitInTwo(name, "/"); |
| return ImmutableList.of(parts.get(0), parts.get(1)); |
| } |
| |
| private static List<String> splitInTwo(String str, String separator) { |
| Iterator<String> iter = Splitter.on(separator).limit(2).split(str).iterator(); |
| return ImmutableList.of( |
| iter.hasNext() ? iter.next() : "", iter.hasNext() ? iter.next() : ""); |
| } |
| |
| /** |
| * Takes an address and a relative name and returns a rooted or relative name. |
| * <p> |
| * If a valid address is supplied then the returned name will always be a rooted name (i.e. |
| * starting with {@code /}), otherwise it may be relative. {@code address} should not start |
| * with a {@code /} and if it does, that prefix will be stripped. |
| */ |
| public static String joinAddressName(String address, String name) { |
| address = CharMatcher.is('/').trimLeadingFrom(address); |
| if (address.isEmpty()) { |
| return clean(name); |
| } |
| if (name.isEmpty()) { |
| return clean("/" + address); |
| } |
| return clean("/" + address + "/" + name); |
| } |
| |
| /** |
| * Takes a variable number of name fragments and concatenates them together using {@code '/'}. |
| * <p> |
| * The returned name is cleaned of multiple adjacent {@code '/'}s. |
| * |
| * @param names name fragments to be concatenated |
| * @return the concatenated (and cleaned) name |
| */ |
| public static String join(String... names) { |
| Iterator<String> iter = Arrays.asList(names).iterator(); |
| for (int i = 0; i < names.length && names[i].isEmpty(); ++i, iter.next()); |
| return clean(Joiner.on("/").join(iter)); |
| } |
| |
| /** |
| * Splits the given name into fragments using {@code '/'} as the separator. |
| * <p> |
| * The returned list is cleaned of empty strings. |
| */ |
| public static List<String> split(String name) { |
| return Splitter.on("/").omitEmptyStrings().splitToList(name); |
| } |
| |
| /** |
| * Removes the suffix (and any connecting {@code /}) from the name. |
| */ |
| public static String trimSuffix(String name, String suffix) { |
| name = clean(name); |
| suffix = clean(suffix); |
| |
| // Easy cases first. |
| if (name.equals(suffix)) { |
| return ""; |
| } |
| if (suffix.length() >= name.length()) { |
| return name; |
| } |
| |
| // A suffix starting with a slash cannot be a partial match. |
| if (suffix.startsWith("/")) { |
| return name; |
| } |
| // At this point suffix is guaranteed not to start with a '/' and |
| // suffix is shorter than name. |
| if (name.endsWith(suffix)) { |
| String prefix = name.substring(0, name.length() - suffix.length()); |
| if (prefix.endsWith("/")) { |
| if (prefix.length() == 1) { |
| return name; |
| } |
| return prefix.substring(0, prefix.length() - 1); |
| } |
| return prefix; |
| } |
| return name; |
| } |
| |
| /** |
| * Reduces multiple adjacent slashes to a single slash and removes any trailing slash. |
| */ |
| public static String clean(String name) { |
| CharMatcher slashMatcher = CharMatcher.is('/'); |
| name = slashMatcher.collapseFrom(name, '/'); |
| if ("/".equals(name)) { |
| return name; |
| } |
| return slashMatcher.trimTrailingFrom(name); |
| } |
| |
| /** |
| * Returns {@code true} iff the provided name is rooted. |
| * <p> |
| * A rooted name is one that starts with a single {@code /} followed by |
| * a non-{@code /}. |
| * <p> |
| * {@code /} on its own is considered rooted. |
| */ |
| public static boolean isRooted(String name) { |
| return name.startsWith("/"); |
| } |
| |
| |
| /** |
| * Returns a string representable as a name element by escaping {@code /}. |
| * |
| * @param name Name to encode. |
| * @return Encoded name. |
| */ |
| public static String encodeAsNameElement(String name) { |
| return escape(name, new char[]{'/'}); |
| } |
| |
| /** |
| * Decodes an encoded name element. |
| * <p> |
| * Note that this is more than the inverse of {@link NamingUtil#encodeAsNameElement} since it |
| * can handle more hex encodings than {@code /} and {@code %}. |
| * This is intentional since we'll most likely want to add other letters to the set to be encoded. |
| * |
| * @param name Name to decode. |
| * @return Decoded name. |
| * @throws IllegalArgumentException if {@code name} is truncated or malformed. |
| */ |
| public static String decodeFromNameElement(String name) { |
| return unescape(name); |
| } |
| |
| private static final String hexDigits = "0123456789ABCDEF"; |
| |
| /** |
| * Encodes a string replacing the characters in {@code special} and {@code %} with a {@code %<hex>} escape. |
| * |
| * @param text Text to escape. |
| * @param special Collection of special characters to escape in {@code text}. |
| * @return Encoded text. |
| */ |
| public static String escape(String text, char[] special) { |
| /* |
| * Note that this function is a modified version of Android's URLEncoder in |
| * Android SDK java/net/URLEncoder.java (https://goo.gl/61z5ZV). |
| * The biggest difference is that, unlike URLEncoder, our code only encodes characters |
| * in {@code special} rather than all non-ASCII characters. |
| * We also do not convert space to +. |
| */ |
| String specialStr = new String(special) + '%'; |
| |
| // Avoid copying the string if it does not have any of the special characters. |
| boolean hasSpecial = false; |
| for (int i = 0; i < text.length(); i++) { |
| char ch = text.charAt(i); |
| if (specialStr.indexOf(ch) >= 0) { |
| hasSpecial = true; |
| break; |
| } |
| } |
| |
| if (!hasSpecial) { |
| return text; |
| } |
| |
| StringBuffer buf = new StringBuffer(); |
| for (int i = 0; i < text.length(); i++) { |
| char ch = text.charAt(i); |
| if (specialStr.indexOf(ch) < 0) { |
| buf.append(ch); |
| } else { |
| byte[] bytes = new String(new char[] { ch }).getBytes(); |
| for (int j = 0; j < bytes.length; j++) { |
| buf.append('%'); |
| buf.append(hexDigits.charAt((bytes[j] & 0xf0) >> 4)); |
| buf.append(hexDigits.charAt(bytes[j] & 0xf)); |
| } |
| } |
| } |
| return buf.toString(); |
| } |
| |
| /** |
| * Decodes {@code}%<hex>} encodings in a string into the relevant character. |
| * |
| * @param text |
| * @return Decoded text. |
| * @throws IllegalArgumentException if the {@code text} is trucated or malformed. |
| */ |
| public static String unescape(String text) { |
| /* |
| * Note that this function is a slightly modified version of Android's URLDecoder in |
| * Android SDK java/net/URLDecoder.java (https://goo.gl/TFrx7E). |
| * The biggest difference is that, unlike URLDecoder, our code does not convert + to space. |
| * We also avoid string copying if text does not encoded to start with. |
| */ |
| |
| // Avoid string copying if text is not encoded to start with. |
| if (text.indexOf('%') < 0){ |
| return text; |
| } |
| |
| StringBuffer result = new StringBuffer(text.length()); |
| ByteArrayOutputStream out = new ByteArrayOutputStream(); |
| for (int i = 0; i < text.length();) { |
| char c = text.charAt(i); |
| if (c == '%') { |
| out.reset(); |
| do { |
| if (i + 2 >= text.length()) { |
| throw new IllegalArgumentException("Truncated or malformed encoded string"); |
| } |
| int d1 = Character.digit(text.charAt(i + 1), 16); |
| int d2 = Character.digit(text.charAt(i + 2), 16); |
| if (d1 == -1 || d2 == -1) { |
| throw new IllegalArgumentException("Truncated or malformed encoded string"); |
| } |
| out.write((byte) ((d1 << 4) + d2)); |
| i += 3; |
| } while (i < text.length() && text.charAt(i) == '%'); |
| result.append(out.toString()); |
| continue; |
| } else { |
| result.append(c); |
| } |
| i++; |
| } |
| return result.toString(); |
| |
| } |
| |
| private NamingUtil() {} |
| } |