/** * Copyright (C) 2009-2013 FoundationDB, LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.foundationdb.util; import com.foundationdb.server.error.InvalidParameterValueException; import com.foundationdb.sql.unparser.NodeToString; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; import com.google.common.io.BaseEncoding; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintWriter; import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.net.JarURLConnection; import java.net.URL; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.util.*; import java.util.Map.Entry; import java.util.jar.JarEntry; /** * String utils. */ public abstract class Strings { public static final String NL = nl(); public static List<String> entriesToString(Map<?, ?> map) { List<String> result = new ArrayList<>(map.size()); for (Map.Entry<?, ?> entry : map.entrySet()) result.add(entry.toString()); return result; } private static class ListAppendable implements Appendable { private final List<String> list; public ListAppendable(List<String> list) { this.list = list; } @Override public Appendable append(CharSequence csq) throws IOException { list.add(csq.toString()); return this; } @Override public Appendable append(CharSequence csq, int start, int end) throws IOException { return append(csq.subSequence(start, end).toString()); } @Override public Appendable append(char c) throws IOException { return append(String.valueOf(c)); } } /** * Gets the system <tt>line.separator</tt> newline. * @return <tt>System.getProperty("line.separator")</tt> */ public static String nl() { String nl = System.getProperty("line.separator"); if (nl == null) { throw new NullPointerException("couldn't find system property line.separator"); } return nl; } /** * Joins the given Strings into a single, newline-delimited String. Newline is the system-dependent one as * defined by the system property <tt>line.separator</tt>. * @param strings the strings * @return the String */ public static String join(Collection<?> strings) { return join(strings, nl()); } /** * Joins the given Strings into a single, newline-delimited String. Newline is the system-dependent one as * defined by the system property <tt>line.separator</tt>. * @param strings the strings * @return the String */ public static String join(Object... strings) { return join(Arrays.asList(strings)); } /** * Joins the given Strings into a single String with the given delimiter. The last String in the list will * not have the delimiter appended. If the list is empty, this returns an empty string. * @param strings a list of strings. May not be null. * @param delimiter the delimiter between strings; this will be inserted <tt>(strings.size() - 1)</tt> times. * May not be null. * @return the joined string */ public static String join(Collection<?> strings, String delimiter) { if (strings.size() == 0) { return ""; } StringBuilder builder = new StringBuilder(30 * strings.size()); // rough guess for capacity! for (Object string : strings) { builder.append(string).append(delimiter); } builder.setLength(builder.length() - delimiter.length()); return builder.toString(); } public static String repeatString(String str, int count) { StringBuilder sb = new StringBuilder(str.length() * count); while (count-- > 0) { sb.append(str); } return sb.toString(); } public static List<String> stringAndSort(Collection<?> inputs) { List<String> results = new ArrayList<>(inputs.size()); for (Object item : inputs) { String asString = stringAndSort(item); results.add(asString); } Collections.sort(results); return results; } public static List<String> stringAndSort(Map<?,?> inputs) { // step 1: get the key-value pairs into a multimap. We need a multimap because multiple keys may have the // same toString. For instance, { 1 : "int", 1L : "long" } would become a multimap { "1" : ["int", "long"] } Multimap<String,String> multiMap = ArrayListMultimap.create(); for (Map.Entry<?,?> inputEntry : inputs.entrySet()) { String keyString = stringAndSort(inputEntry.getKey()); String valueString = stringAndSort(inputEntry.getValue()); multiMap.put(keyString, valueString); } // step 2: Flatten the multimap into a Map<String,String>, sorting by keys as you go. Map<String,String> sortedAndFlattened = new TreeMap<>(); for (Entry<String,Collection<String>> multiMapEntry : multiMap.asMap().entrySet()) { String keyString = multiMapEntry.getKey(); String valueString = stringAndSort(multiMapEntry.getValue()).toString(); String duplicate = sortedAndFlattened.put(keyString, valueString); assert duplicate == null : duplicate; } // step 3: Flatten the map into a List<String> List<String> results = new ArrayList<>(inputs.size()); for (Entry<String,String> entry : sortedAndFlattened.entrySet()) { results.add(entry.toString()); } return results; } private static String stringAndSort(Object item) { if (item instanceof Collection) { return stringAndSort((Collection<?>)item).toString(); } else if (item instanceof Map) { return stringAndSort((Map<?,?>)item).toString(); } else { return String.valueOf(item); } } /** * Dumps the content of a resource into a List<String>, where each element is one line of the resource. * @param forClass the class whose resource we should get; if null, will get the default * <tt>ClassLoader.getSystemResourceAsStream</tt>. * @param path the name of the resource * @return a list of lines in the resource * @throws IOException if the given resource doesn't exist or can't be properly read */ public static List<String> dumpResource(Class<?> forClass, String path) throws IOException { InputStream is = (forClass == null) ? ClassLoader.getSystemResourceAsStream(path) : forClass.getResourceAsStream(path); if (is == null) { throw new FileNotFoundException("For class " + forClass + ": " + path); } return readStream(is); } public static List<String> dumpURLs(Enumeration<URL> urls) throws IOException { List<String> result = new ArrayList<>(); while (urls.hasMoreElements()) { URL next = urls.nextElement(); LOG.debug("reading URL: {}", next); boolean readAsStream = true; if ("jar".equals(next.getProtocol())) { JarURLConnection connection = (JarURLConnection)next.openConnection(); if (connection.getJarEntry().isDirectory()) { readJarConnectionTo(connection, result); readAsStream = false; } } if (readAsStream) { InputStream is = next.openStream(); readStreamTo(is, new ListAppendable(result), false); } } return result; } @SuppressWarnings("unused") // primarily useful in debuggers public static String[] dumpException(Throwable t) { StringWriter stringWriter = new StringWriter(); PrintWriter printWriter = new PrintWriter(stringWriter); t.printStackTrace(printWriter); printWriter.flush(); stringWriter.flush(); return stringWriter.toString().split("\\n"); } public static String toOctal(byte[] bytes){ StringBuilder out = new StringBuilder(); for (byte b : bytes) { out.append(String.format("\\%03o", b)); } return out.toString(); } public static String hex(byte[] bytes) { return hex(bytes, 0, bytes.length); } public static String hex(ByteSource byteSource) { return hex(byteSource.byteArray(), byteSource.byteArrayOffset(), byteSource.byteArrayLength()); } public static String hex(byte[] bytes, int start, int length) { return BaseEncoding.base16().encode(bytes, start, length); } /** For example, '0' returns 0, 'a' or 'A' returns 10, etc */ private static int hexCharToInt(char c) { int lower = c | 32; if(lower >= '0' && lower <= '9') return lower - '0'; if(lower >= 'a' && lower <= 'f') return 10 + lower - 'a'; throw new InvalidParameterValueException("Invalid HEX digit: " + c); } /** For example, ('2','0') returns 32 */ private static byte hexCharsToByte(char highChar, char lowChar) { return (byte)((hexCharToInt(highChar) << 4) + hexCharToInt(lowChar)); } public static ByteSource parseHexWithout0x (String st) { int odd = st.length() & 0x01; int outputLen = (st.length() >> 1) + odd; byte ret[] = new byte[outputLen]; int si = 0, ri = 0; if (odd == 1) { ret[ri++] = (byte)hexCharToInt(st.charAt(si++)); } while(ri < ret.length) { ret[ri++] = hexCharsToByte(st.charAt(si++), st.charAt(si++)); } return new WrappingByteSource(ret); } public static ByteSource parseHex(String string) { if (!string.startsWith("0x")) { throw new RuntimeException("not a hex string"); } byte[] ret = new byte[ (string.length()-2) / 2 ]; int resultIndex = 0; for (int strIndex=2; strIndex < string.length(); ++strIndex) { final char strChar = string.charAt(strIndex); if (!Character.isWhitespace(strChar)) { int high = (Character.digit(strChar, 16)) << 4; char lowChar = string.charAt(++strIndex); int low = (Character.digit(lowChar, 16)); ret[resultIndex++] = (byte) (low + high); } } return new WrappingByteSource().wrap(ret, 0, resultIndex); } public static List<String> readStream(InputStream is) throws IOException { List<String> result = new ArrayList<>(); readStreamTo(is, new ListAppendable(result), false); return result; } public static void readStreamTo(InputStream is, Appendable out, boolean keepNL) throws IOException { readerTo(bufferedReader(is), out, keepNL); } private static void readerTo(BufferedReader reader, Appendable out, boolean keepNL) throws IOException { try { if(keepNL) { CharBuffer buffer = CharBuffer.allocate(1024); while(reader.read(buffer) != -1) { buffer.flip(); out.append(buffer); buffer.clear(); } } else { String line; while((line = reader.readLine()) != null) { out.append(line); } } } finally { reader.close(); } } private static void readJarConnectionTo(JarURLConnection connection, List<String> result) throws IOException { assert connection.getJarEntry().isDirectory() : "not a dir: " + connection.getJarEntry(); // put into entries only the children of the connection's entry, and trim off the entry prefix String base = connection.getEntryName(); Enumeration<JarEntry> enumeration = connection.getJarFile().entries(); while (enumeration.hasMoreElements()) { JarEntry entry = enumeration.nextElement(); if (entry.getName().startsWith(base)) result.add(entry.getName().substring(base.length())); } } public static <T> String toString(Multimap<T,?> map) { StringBuilder sb = new StringBuilder(); for (Iterator<T> keysIter = map.keySet().iterator(); keysIter.hasNext(); ) { T key = keysIter.next(); sb.append(key).append(" => "); for (Iterator<?> valsIter = map.get(key).iterator(); valsIter.hasNext(); ) { sb.append(valsIter.next()); if (valsIter.hasNext()) sb.append(", "); } if (keysIter.hasNext()) sb.append(nl()); } return sb.toString(); } public static String stripr(String input, String suffix) { if (input == null || suffix == null) return input; return input.endsWith(suffix) ? input.substring(0, input.length() - suffix.length()) : input; } private static final Logger LOG = LoggerFactory.getLogger(Strings.class); public static List<String> dumpFile(File file) throws IOException { List<String> results = new ArrayList<>(); readerTo(bufferedReader(new FileInputStream(file)), new ListAppendable(results), false); return results; } public static String dumpFileToString(File file) throws IOException { StringBuilder builder = new StringBuilder(); readerTo(bufferedReader(new FileInputStream(file)), builder, true); return builder.toString(); } public static List<String> mapToString(Collection<?> collection) { // are lambdas here yet?! List<String> strings = new ArrayList<>(collection.size()); for (Object o : collection) strings.add(String.valueOf(o)); return strings; } public static boolean equalCharsets(Charset one, String two) { return one.name().equals(two) || one.equals(Charset.forName(two)); } private static BufferedReader bufferedReader(InputStream is) { try { return new BufferedReader(new InputStreamReader(is, "UTF-8")); } catch(UnsupportedEncodingException e) { throw new RuntimeException(e); } } public static String formatMD5(byte[] md5, boolean toLowerCase) { BaseEncoding encoder = toLowerCase ? BaseEncoding.base16().lowerCase() : BaseEncoding.base16().upperCase(); return encoder.encode(md5); } public static String truncateIfNecessary(String str, int codePointCount) { // Try to avoid scanning the string for surrogates, which are rare in the wild. int nchars = str.length(); if (nchars <= codePointCount) return str; int ncode = str.codePointCount(0, nchars); if (ncode <= codePointCount) return str; if (nchars == ncode) return str.substring(0, codePointCount); else return str.substring(0, str.offsetByCodePoints(0, codePointCount)); } public static String toBase64(byte[] bytes) { return toBase64(bytes, 0, bytes.length); } public static String toBase64(byte[] bytes, int offset, int length) { return BaseEncoding.base64().encode(bytes, offset, length); } public static byte[] fromBase64(CharSequence cs) { return BaseEncoding.base64().decode(cs); } /** * Split a period delimited string of identifiers into an array of * constituent pieces. Up to {@code maxParts} many identifiers will * be returned and non-quoted identifiers will be lower-cased. * <p> * For example, * <pre> * (test.t, 1) => [test] * (test.t, 2) => [test, t] * (test.t, 3) => ["", test, t] * (A.B, 2) => [a, b] * ("a.B".t, 2) => [a.B, t] * </pre> * </p> */ public static String[] parseQualifiedName(String arg, int maxParts) { assert maxParts > 0 : maxParts; String[] result = new String[maxParts]; int resIndex = 0; char lastQuote = 0; int prevEnd = 0; for(int i = 0; i < arg.length(); ++i) { char c = arg.charAt(i); boolean take = false; boolean toLower = true; if(lastQuote != 0) { if(c == lastQuote) { take = true; toLower = false; lastQuote = 0; } } else if(c == '"' || c == '`') { lastQuote = c; prevEnd = i + 1; } else if(c == '.') { take = true; } if(take) { if(prevEnd < i) { result[resIndex++] = consumeIdentifier(arg, prevEnd, i, toLower); } prevEnd = i + 1; } } if((resIndex < maxParts) && (prevEnd < arg.length())) { result[resIndex++] = consumeIdentifier(arg, prevEnd, arg.length(), lastQuote == 0); } int diff = maxParts - resIndex; if(diff > 0) { // Shift found and empty fill missing System.arraycopy(result, 0, result, maxParts - resIndex, resIndex); for(int i = 0; i < diff; ++i) { result[i] = ""; } } return result; } public static String quotedIdent(String s, char quote, boolean force) { String quoteS = Character.toString(quote); if (s.contains(quoteS)){ s = s.replaceAll( ("[" + quoteS + "]") , quoteS + quoteS ); } if (!force && !NodeToString.isReserved(s) && s.matches("[a-z][_a-z0-9$]*") ) { return s; } else { return quote + s + quote; } } public static String escapeIdentifier(String s) { return String.format("\"%s\"", s.replace("\"", "\"\"")); } // // Internal // private static String consumeIdentifier(String arg, int begin, int end, boolean toLower) { String s = arg.substring(begin, end); if(toLower) { s = s.toLowerCase(); } return s; } }