All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.javascript.jscomp.JsMessage Maven / Gradle / Ivy

/*
 * Copyright 2006 The Closure Compiler Authors.
 *
 * 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.javascript.jscomp;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.common.annotations.GwtIncompatible;
import com.google.common.base.Ascii;
import com.google.javascript.jscomp.parsing.parser.util.format.SimpleFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;

/**
 * A representation of a translatable message in JavaScript source code.
 *
 * 

Instances are created using a {@link JsMessage.Builder}, * like this: *

 * JsMessage m = new JsMessage.Builder(key)
 *     .appendPart("Hi ")
 *     .appendPlaceholderReference("firstName")
 *     .appendPart("!")
 *     .setDesc("A welcome message")
 *     .build();
 * 
*/ public final class JsMessage { /** * Message style that could be used for JS code parsing. * The enum order is from most relaxed to most restricted. */ public enum Style { LEGACY, // All legacy code is completely OK RELAX, // You allowed to use legacy code but it would be reported as warn CLOSURE; // Any legacy code is prohibited /** * Calculates current messages {@link Style} based on the given arguments. * * @param useClosure if true then use closure style, otherwise not * @param allowLegacyMessages if true then allow legacy messages otherwise * not * @return the message style based on the given arguments */ static Style getFromParams(boolean useClosure, boolean allowLegacyMessages) { if (useClosure) { return allowLegacyMessages ? RELAX : CLOSURE; } else { return LEGACY; } } } private static final String MESSAGE_REPRESENTATION_FORMAT = "{$%s}"; private final String key; private final String id; private final List parts; private final Set placeholders; private final String desc; private final String alternateId; private final boolean hidden; private final String meaning; private final String sourceName; private final boolean isAnonymous; private final boolean isExternal; /** * Creates an instance. Client code should use a {@link JsMessage.Builder}. * * @param key a key that should identify this message in sources; typically it is the message's * name (e.g. {@code "MSG_HELLO"}). * @param id an id that *uniquely* identifies the message in the bundle. It could be either the * message name or id generated from the message content. * @param meaning The user-specified meaning of the message. May be null if the user did not * specify an explicit meaning. * @param alternateId Alternate ID to be used if a translation for {@code id} is missing. */ private JsMessage( String sourceName, String key, boolean isAnonymous, boolean isExternal, String id, List parts, Set placeholders, String desc, boolean hidden, String meaning, String alternateId) { checkState(key != null); checkState(id != null); this.key = key; this.id = id; this.parts = Collections.unmodifiableList(parts); this.placeholders = Collections.unmodifiableSet(placeholders); this.desc = desc; this.alternateId = alternateId; this.hidden = hidden; this.meaning = meaning; this.sourceName = sourceName; this.isAnonymous = isAnonymous; this.isExternal = isExternal; } /** * Gets the message's sourceName. */ public String getSourceName() { return sourceName; } /** * Gets the message's key, or name (e.g. {@code "MSG_HELLO"}). */ public String getKey() { return key; } public boolean isAnonymous() { return isAnonymous; } public boolean isExternal() { return isExternal; } /** * Gets the message's id, or name (e.g. {@code "92430284230902938293"}). */ public String getId() { return id; } /** * Gets the message's alternate ID (e.g. {@code "92430284230902938293"}), if available. This will * be used if a translation for `id` is not available. */ @Nullable public String getAlternateId() { return alternateId; } /** * Gets the description associated with this message, intended to help * translators, or null if this message has no description. */ public String getDesc() { return desc; } /** * Gets the meaning annotated to the message, intended to force different * translations. */ public String getMeaning() { return meaning; } /** * Gets whether this message should be hidden from volunteer translators (to * reduce the chances of a new feature leak). */ public boolean isHidden() { return hidden; } /** * Gets a read-only list of the parts of this message. Each part is either a * {@link String} or a {@link PlaceholderReference}. */ public List parts() { return parts; } /** Gets a read-only set of the registered placeholders in this message. */ public Set placeholders() { return placeholders; } @Override public String toString() { StringBuilder sb = new StringBuilder(); for (CharSequence p : parts) { sb.append(p.toString()); } return sb.toString(); } /** @return false iff the message is represented by empty string. */ public boolean isEmpty() { for (CharSequence part : parts) { if (part.length() > 0) { return false; } } return true; } @Override public boolean equals(Object o) { if (o == this) { return true; } if (!(o instanceof JsMessage)) { return false; } JsMessage m = (JsMessage) o; return id.equals(m.id) && key.equals(m.key) && isAnonymous == m.isAnonymous && parts.equals(m.parts) && (meaning == null ? m.meaning == null : meaning.equals(m.meaning)) && placeholders.equals(m.placeholders) && (desc == null ? m.desc == null : desc.equals(m.desc)) && (sourceName == null ? m.sourceName == null : sourceName.equals(m.sourceName)) && hidden == m.hidden; } @Override public int hashCode() { int hash = key.hashCode(); hash = 31 * hash + (isAnonymous ? 1 : 0); hash = 31 * hash + id.hashCode(); hash = 31 * hash + parts.hashCode(); hash = 31 * hash + (desc != null ? desc.hashCode() : 0); hash = 31 * hash + (hidden ? 1 : 0); hash = 31 * hash + (sourceName != null ? sourceName.hashCode() : 0); return hash; } /** A reference to a placeholder in a translatable message. */ public static class PlaceholderReference implements CharSequence { private final String name; PlaceholderReference(String name) { this.name = name; } @Override public int length() { return name.length(); } @Override public char charAt(int index) { return name.charAt(index); } @Override public CharSequence subSequence(int start, int end) { return name.subSequence(start, end); } public String getName() { return name; } @Override public String toString() { return SimpleFormat.format(MESSAGE_REPRESENTATION_FORMAT, name); } @Override public boolean equals(Object o) { return o == this || (o instanceof PlaceholderReference && name.equals(((PlaceholderReference) o).name)); } @Override public int hashCode() { return 31 * name.hashCode(); } } /** * Contains functionality for creating JS messages. Generates authoritative * keys and fingerprints for a message that must stay constant over time. * * This implementation correctly processes unnamed messages and creates a key * for them that looks like {@code MSG_};. */ @GwtIncompatible("java.util.regex") public static class Builder { // Allow arbitrary suffixes to allow for local variable disambiguation. private static final String MSG_EXTERNAL_PREFIX = "MSG_EXTERNAL_"; /** * @return an external message id or null if this is not an * external message identifier */ private static String getExternalMessageId(String identifier) { if (identifier.startsWith(MSG_EXTERNAL_PREFIX)) { int start = MSG_EXTERNAL_PREFIX.length(); int end = start; for (; end < identifier.length(); end++) { char c = identifier.charAt(end); if (c > '9' || c < '0') { break; } } if (end > start) { return identifier.substring(start, end); } } return null; } private String key; private String meaning; private String desc; private boolean hidden; private String alternateId; private final List parts = new ArrayList<>(); private final Set placeholders = new HashSet<>(); private String sourceName; public Builder() { this(null); } /** Creates an instance. */ public Builder(String key) { this.key = key; } /** Gets the message's key (e.g. {@code "MSG_HELLO"}). */ public String getKey() { return key; } /** * @param key a key that should uniquely identify this message; typically * it is the message's name (e.g. {@code "MSG_HELLO"}). */ public Builder setKey(String key) { this.key = key; return this; } /** * @param sourceName The message's sourceName. */ public Builder setSourceName(String sourceName) { this.sourceName = sourceName; return this; } /** * Appends a placeholder reference to the message */ public Builder appendPlaceholderReference(String name) { checkNotNull(name, "Placeholder name could not be null"); parts.add(new PlaceholderReference(name)); placeholders.add(name); return this; } /** Appends a translatable string literal to the message. */ public Builder appendStringPart(String part) { checkNotNull(part, "String part of the message could not be null"); parts.add(part); return this; } /** Returns the message registered placeholders */ public Set getPlaceholders() { return placeholders; } /** Sets the description of the message, which helps translators. */ public Builder setDesc(String desc) { this.desc = desc; return this; } /** * Sets the programmer-specified meaning of this message, which * forces this message to translate differently. */ public Builder setMeaning(String meaning) { this.meaning = meaning; return this; } /** Sets the alternate message ID, to be used if the primary ID is not yet translated. */ public Builder setAlternateId(String alternateId) { this.alternateId = alternateId; return this; } /** Sets whether the message should be hidden from volunteer translators. */ public Builder setIsHidden(boolean hidden) { this.hidden = hidden; return this; } /** Gets whether at least one part has been appended. */ public boolean hasParts() { return !parts.isEmpty(); } public List getParts() { return parts; } public JsMessage build() { return build(null); } public JsMessage build(IdGenerator idGenerator) { boolean isAnonymous = false; boolean isExternal = false; String id = null; if (getKey() == null) { // Before constructing a message we need to change unnamed messages name // to the unique one. key = JsMessageVisitor.MSG_PREFIX + fingerprint(getParts()); isAnonymous = true; } if (!isAnonymous) { String externalId = getExternalMessageId(key); if (externalId != null) { isExternal = true; id = externalId; } } if (!isExternal) { String defactoMeaning = meaning != null ? meaning : key; id = idGenerator == null ? defactoMeaning : idGenerator.generateId(defactoMeaning, parts); } return new JsMessage( sourceName, key, isAnonymous, isExternal, id, parts, placeholders, desc, hidden, meaning, alternateId); } /** * Generates a compact uppercase alphanumeric text representation of a * 63-bit fingerprint of the content parts of a message. */ private static String fingerprint(List messageParts) { StringBuilder sb = new StringBuilder(); for (CharSequence part : messageParts) { if (part instanceof JsMessage.PlaceholderReference) { sb.append(part.toString()); } else { sb.append(part); } } long nonnegativeHash = Long.MAX_VALUE & Hash.hash64(sb.toString()); return Ascii.toUpperCase(Long.toString(nonnegativeHash, 36)); } } /** * This class contains routines for hashing. * *

The hash takes a byte array representing arbitrary data (a * number, String, or Object) and turns it into a small, hopefully * unique, number. There are additional convenience functions which * hash int, long, and String types. * *

Note: this hash has weaknesses in the two * most-significant key bits and in the three least-significant seed * bits. The weaknesses are small and practically speaking, will not * affect the distribution of hash values. Still, it would be good * practice not to choose seeds 0, 1, 2, 3, ..., n to yield n, * independent hash functions. Use pseudo-random seeds instead. * *

This code is based on the work of Craig Silverstein and Sanjay * Ghemawat in, then forked from com.google.common. * *

The original code for the hash function is courtesy * Bob Jenkins. * *

TODO(anatol): Add stream hashing functionality. */ static final class Hash { private Hash() {} /** Default hash seed (64 bit) */ private static final long SEED64 = 0x2b992ddfa23249d6L; // part of pi, arbitrary /** Hash constant (64 bit) */ private static final long CONSTANT64 = 0xe08c1d668b756f82L; // part of golden ratio, arbitrary /****************** * STRING HASHING * ******************/ /** * Hash a string to a 64 bit value. The digits of pi are used for * the hash seed. * * @param value the string to hash * @return 64 bit hash value */ static long hash64(@Nullable String value) { return hash64(value, SEED64); } /** * Hash a string to a 64 bit value using the supplied seed. * * @param value the string to hash * @param seed the seed * @return 64 bit hash value */ private static long hash64(@Nullable String value, long seed) { if (value == null) { return hash64(null, 0, 0, seed); } return hash64(value.getBytes(UTF_8), seed); } /** * Hash byte array to a 64 bit value using the supplied seed. * * @param value the bytes to hash * @param seed the seed * @return 64 bit hash value */ private static long hash64(byte[] value, long seed) { return hash64(value, 0, value == null ? 0 : value.length, seed); } /** * Hash byte array to a 64 bit value using the supplied seed. * * @param value the bytes to hash * @param offset the starting position of value where bytes are * used for the hash computation * @param length number of bytes of value that are used for the * hash computation * @param seed the seed * @return 64 bit hash value */ @SuppressWarnings("fallthrough") private static long hash64( byte[] value, int offset, int length, long seed) { long a = CONSTANT64; long b = a; long c = seed; int keylen; for (keylen = length; keylen >= 24; keylen -= 24, offset += 24) { a += word64At(value, offset); b += word64At(value, offset + 8); c += word64At(value, offset + 16); // Mix a -= b; a -= c; a ^= c >>> 43; b -= c; b -= a; b ^= a << 9; c -= a; c -= b; c ^= b >>> 8; a -= b; a -= c; a ^= c >>> 38; b -= c; b -= a; b ^= a << 23; c -= a; c -= b; c ^= b >>> 5; a -= b; a -= c; a ^= c >>> 35; b -= c; b -= a; b ^= a << 49; c -= a; c -= b; c ^= b >>> 11; a -= b; a -= c; a ^= c >>> 12; b -= c; b -= a; b ^= a << 18; c -= a; c -= b; c ^= b >>> 22; } c += length; if (keylen >= 16) { if (keylen == 23) { c += ((long) value[offset + 22]) << 56; } if (keylen >= 22) { c += (value[offset + 21] & 0xffL) << 48; } if (keylen >= 21) { c += (value[offset + 20] & 0xffL) << 40; } if (keylen >= 20) { c += (value[offset + 19] & 0xffL) << 32; } if (keylen >= 19) { c += (value[offset + 18] & 0xffL) << 24; } if (keylen >= 18) { c += (value[offset + 17] & 0xffL) << 16; } if (keylen >= 17) { c += (value[offset + 16] & 0xffL) << 8; // the first byte of c is reserved for the length } if (keylen >= 16) { b += word64At(value, offset + 8); a += word64At(value, offset); } } else if (keylen >= 8) { if (keylen == 15) { b += (value[offset + 14] & 0xffL) << 48; } if (keylen >= 14) { b += (value[offset + 13] & 0xffL) << 40; } if (keylen >= 13) { b += (value[offset + 12] & 0xffL) << 32; } if (keylen >= 12) { b += (value[offset + 11] & 0xffL) << 24; } if (keylen >= 11) { b += (value[offset + 10] & 0xffL) << 16; } if (keylen >= 10) { b += (value[offset + 9] & 0xffL) << 8; } if (keylen >= 9) { b += (value[offset + 8] & 0xffL); } if (keylen >= 8) { a += word64At(value, offset); } } else { if (keylen == 7) { a += (value[offset + 6] & 0xffL) << 48; } if (keylen >= 6) { a += (value[offset + 5] & 0xffL) << 40; } if (keylen >= 5) { a += (value[offset + 4] & 0xffL) << 32; } if (keylen >= 4) { a += (value[offset + 3] & 0xffL) << 24; } if (keylen >= 3) { a += (value[offset + 2] & 0xffL) << 16; } if (keylen >= 2) { a += (value[offset + 1] & 0xffL) << 8; } if (keylen >= 1) { a += (value[offset + 0] & 0xffL); // case 0: nothing left to add } } return mix64(a, b, c); } private static long word64At(byte[] bytes, int offset) { return (bytes[offset + 0] & 0xffL) + ((bytes[offset + 1] & 0xffL) << 8) + ((bytes[offset + 2] & 0xffL) << 16) + ((bytes[offset + 3] & 0xffL) << 24) + ((bytes[offset + 4] & 0xffL) << 32) + ((bytes[offset + 5] & 0xffL) << 40) + ((bytes[offset + 6] & 0xffL) << 48) + ((bytes[offset + 7] & 0xffL) << 56); } /** * Mixes longs a, b, and c, and returns the final value of c. */ private static long mix64(long a, long b, long c) { a -= b; a -= c; a ^= c >>> 43; b -= c; b -= a; b ^= a << 9; c -= a; c -= b; c ^= b >>> 8; a -= b; a -= c; a ^= c >>> 38; b -= c; b -= a; b ^= a << 23; c -= a; c -= b; c ^= b >>> 5; a -= b; a -= c; a ^= c >>> 35; b -= c; b -= a; b ^= a << 49; c -= a; c -= b; c ^= b >>> 11; a -= b; a -= c; a ^= c >>> 12; b -= c; b -= a; b ^= a << 18; c -= a; c -= b; c ^= b >>> 22; return c; } } /** ID generator */ public interface IdGenerator { /** * Generate the ID for the message. Messages with the same messageParts * and meaning will get the same id. Messages with the same id * will get the same translation. * * @param meaning The programmer-specified meaning. If no {@code @meaning} * annotation appears, we will use the name of the variable it's * assigned to. If the variable is unnamed, then we will just * use a fingerprint of the message. * @param messageParts The parts of the message, including the main * message text. */ String generateId(String meaning, List messageParts); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy