Java tutorial
// 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() { } }