Java tutorial
/* * Copyright 2008 Google Inc. * * 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.template.soy.base.internal; import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.base.Preconditions; import com.google.common.collect.Sets; import com.google.common.hash.Hashing; import java.io.File; import java.util.Set; import java.util.regex.Pattern; /** * Base utilities for Soy code. * * <p> Important: Do not use outside of Soy code (treat as superpackage-private). * */ public class BaseUtils { private BaseUtils() { } /** Used by {@code ensureDirsExistInPath()}. Keeps track of known existing directory paths. */ private static final Set<String> KNOWN_EXISTING_DIRS = Sets.newHashSet(); /** Regular expression for an identifier. */ public static final String IDENT_RE = "[a-zA-Z_][a-zA-Z_0-9]*"; /** Pattern for an identifier. */ private static final Pattern IDENT_PATTERN = Pattern.compile(IDENT_RE); /** Pattern for an identifier with leading dot. */ private static final Pattern IDENT_WITH_LEADING_DOT_PATTERN = Pattern.compile("[.]" + IDENT_RE); /** Regular expression for a dotted identifier. */ public static final String DOTTED_IDENT_RE = IDENT_RE + "(?:[.]" + IDENT_RE + ")*"; /** Pattern for a dotted identifier. */ private static final Pattern DOTTED_IDENT_PATTERN = Pattern.compile(DOTTED_IDENT_RE); /** Regular expression for an identifier that also allows dashes. */ private static final String DASHED_IDENT_RE = IDENT_RE + "(?:[-][a-zA-Z_0-9]*)*"; /** Pattern for an identifier that allows either dots or dashes, but not both. */ private static final Pattern DOTTED_OR_DASHED_IDENT_PATTERN = Pattern .compile(String.format("(?:%s)|(?:%s)", DOTTED_IDENT_RE, DASHED_IDENT_RE)); /** Pattern for a leading or trailing underscore. */ private static final Pattern LEADING_OR_TRAILING_UNDERSCORE_PATTERN = Pattern.compile("^_+|_+\\Z"); /** Pattern for places to insert underscores to make an identifier name underscore-separated. */ private static final Pattern WORD_BOUNDARY_IN_IDENT_PATTERN = Pattern.compile("(?<= [a-zA-Z])(?= [A-Z][a-z])" + // <letter>_<upper><lower> "| (?<= [a-zA-Z])(?= [0-9])" + // <letter>_<digit> "| (?<= [0-9])(?= [a-zA-Z])", // <digit>_<letter> Pattern.COMMENTS); /** Pattern for consecutive underscores. */ private static final Pattern CONSECUTIVE_UNDERSCORES_PATTERN = Pattern.compile("_ _ _*", Pattern.COMMENTS); /** Hex digits for Soy strings (requires upper-case hex digits). */ private static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; /** * Ensures that the directories in the given path exist, creating them if necessary. * * Note: If the path does not end with the separator char (slash in Linux), then the name at the * end is assumed to be the file name, so directories are only created down to its parent. * * @param path The path for which to ensure directories exist. */ public static void ensureDirsExistInPath(String path) { if (path == null || path.length() == 0) { throw new AssertionError("ensureDirsExistInPath called with null or empty path."); } String dirPath = (path.charAt(path.length() - 1) == File.separatorChar) ? path.substring(0, path.length() - 1) : (new File(path)).getParent(); if (dirPath == null || KNOWN_EXISTING_DIRS.contains(dirPath)) { return; // known to exist } else { (new File(dirPath)).mkdirs(); KNOWN_EXISTING_DIRS.add(dirPath); } } /** * Determines whether the given string is an identifier. * * <p> An identifier must start with a letter or underscore and must only contain letters, digits, * and underscores (i.e. it must match the regular expression {@code [A-Za-z_][A-Za-z_0-9]*}). * * @param s The string to check. * @return True if the given string is an identifier. */ public static boolean isIdentifier(String s) { return IDENT_PATTERN.matcher(s).matches(); } /** * Determines whether the given string is a dot followed by an identifier. * * @param s The string to check. * @return True if the given string is a dot followed by an identifier. */ public static boolean isIdentifierWithLeadingDot(String s) { return IDENT_WITH_LEADING_DOT_PATTERN.matcher(s).matches(); } /** * Determines whether the given string is a "dotted or dashed" identifier. * This allows dots, or dashes, but not both. * * @param s The string to check. * @return True if the given string is a dotted-or-dashed identifier. */ public static boolean isDottedOrDashedIdent(String s) { return DOTTED_OR_DASHED_IDENT_PATTERN.matcher(s).matches(); } /** * Determines whether the given string is a dotted identifier (e.g. {@code boo.foo0._goo}). A * dotted identifier is not required to have dots (i.e. a simple identifier qualifies as a dotted * identifier). * * @param s The string to check. * @return True if the given string is a dotted identifier (e.g. {@code boo.foo0._goo}). */ public static boolean isDottedIdentifier(String s) { return DOTTED_IDENT_PATTERN.matcher(s).matches(); } /** * Gets the part after the last dot in a dotted identifier. If there are no dots, returns the * whole input string. * <p> Important: The input must be a dotted identifier. This is not checked. */ public static String extractPartAfterLastDot(String dottedIdent) { int lastDotIndex = dottedIdent.lastIndexOf('.'); return (lastDotIndex == -1) ? dottedIdent : dottedIdent.substring(lastDotIndex + 1); } /** * Converts an identifier to upper-underscore format. The identifier must start with a letter or * underscore and must only contain letters, digits, and underscores (i.e. it must match the * regular expression {@code [A-Za-z_][A-Za-z_0-9]*}). * * @param ident The identifer to convert. * @return The identifier in upper-underscore format. */ public static String convertToUpperUnderscore(String ident) { ident = LEADING_OR_TRAILING_UNDERSCORE_PATTERN.matcher(ident).replaceAll(""); ident = WORD_BOUNDARY_IN_IDENT_PATTERN.matcher(ident).replaceAll("_"); ident = CONSECUTIVE_UNDERSCORES_PATTERN.matcher(ident).replaceAll("_"); return ident.toUpperCase(); } /** * Builds a Soy string literal for this string value (including the surrounding single quotes). * Note that Soy string syntax is a subset of JS string syntax, so the result should also be a * valid JS string. * * Adapted from StringUtil.javaScriptEscape(). * * @param value The string value to escape. * @param shouldEscapeToAscii Whether to escape non-ASCII characters as Unicode hex escapes * (backslash + 'u' + 4 hex digits). * @return A Soy string literal for this string value (including the surrounding single quotes). */ public static String escapeToSoyString(String value, boolean shouldEscapeToAscii) { // StringUtil.javaScriptEscape() is meant to be compatible with JS string syntax, which is a // superset of the Soy expression string syntax, so we can't depend on it to properly escape a // Soy expression string literal. For example, they switched the default character escaping // to octal to save a few bytes, but octal escapes are not allowed in Soy syntax. I'm rewriting // the code here in a correct way for Soy. int len = value.length(); StringBuilder out = new StringBuilder(len * 9 / 8); out.append('\''); int codePoint; for (int i = 0; i < len; i += Character.charCount(codePoint)) { codePoint = value.codePointAt(i); switch (codePoint) { case '\n': out.append("\\n"); break; case '\r': out.append("\\r"); break; case '\t': out.append("\\t"); break; case '\b': out.append("\\b"); break; case '\f': out.append("\\f"); break; case '\\': out.append("\\\\"); break; case '\'': out.append("\\'"); break; case '"': out.append('"'); break; // note: don't escape double quotes in Soy strings default: // If shouldEscapeToAscii, then hex escape characters outside the range 0x20 to 0x7F. if (shouldEscapeToAscii && (codePoint < 0x20 || codePoint >= 0x7F)) { appendHexEscape(out, codePoint); } else { out.appendCodePoint(codePoint); } break; } } out.append('\''); return out.toString(); } /** * Appends the Unicode hex escape sequence for the given code point (backslash + 'u' + 4 hex * digits) to the given StringBuilder. * * Note: May append 2 escape sequences (surrogate pair) in the case of a supplementary character * (outside the Unicode BMP). * * Adapted from StringUtil.appendHexJavaScriptRepresentation(). * * @param out The StringBuilder to append to. * @param codePoint The Unicode code point whose hex escape sequence to append. */ public static void appendHexEscape(StringBuilder out, int codePoint) { if (Character.isSupplementaryCodePoint(codePoint)) { // Handle supplementary unicode values which are not representable in // javascript. We deal with these by escaping them as two 4B sequences // so that they will round-trip properly when sent from java to javascript // and back. char[] surrogates = Character.toChars(codePoint); appendHexEscape(out, surrogates[0]); appendHexEscape(out, surrogates[1]); } else { out.append("\\u").append(HEX_DIGITS[(codePoint >>> 12) & 0xF]) .append(HEX_DIGITS[(codePoint >>> 8) & 0xF]).append(HEX_DIGITS[(codePoint >>> 4) & 0xF]) .append(HEX_DIGITS[codePoint & 0xF]); } } /** * Computes the SHA-1 hash value of the input string's UTF-8 representation and returns the first * numBits bits of the result as a hex value in string form. * * @param strToHash The string to compute SHA-1 of. * @param numBits The number of bits worth to return. Must be a positive number at most 160 and * divisible by 8 (since we process the result 8 bits at a time). * @return The partial SHA-1 hash value as a hex string. */ public static String computePartialSha1AsHexString(String strToHash, int numBits) { Preconditions.checkArgument(numBits > 0 && numBits <= 160 && numBits % 8 == 0); int numBytes = numBits / 8; return Hashing.sha1().hashString(strToHash, UTF_8).toString().substring(0, numBytes * 2); } }