|  | // Copyright 2016 The Bazel Authors. All rights reserved. | 
|  | // | 
|  | // 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.devtools.build.lib.packages; | 
|  |  | 
|  | import com.google.common.annotations.VisibleForTesting; | 
|  | import com.google.common.base.Joiner; | 
|  | import com.google.common.base.Objects; | 
|  | import com.google.common.base.Preconditions; | 
|  | import com.google.common.collect.ImmutableCollection; | 
|  | import com.google.common.collect.ImmutableSortedMap; | 
|  | import com.google.common.collect.Ordering; | 
|  | import com.google.devtools.build.lib.events.Location; | 
|  | import com.google.devtools.build.lib.skylarkbuildapi.StructApi; | 
|  | import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter; | 
|  | import com.google.devtools.build.lib.syntax.ClassObject; | 
|  | import com.google.devtools.build.lib.syntax.Environment; | 
|  | import com.google.devtools.build.lib.syntax.EvalException; | 
|  | import com.google.devtools.build.lib.syntax.EvalUtils; | 
|  | import com.google.devtools.build.lib.syntax.Printer; | 
|  | import com.google.devtools.build.lib.syntax.Runtime; | 
|  | import com.google.devtools.build.lib.syntax.SkylarkDict; | 
|  | import com.google.devtools.build.lib.syntax.SkylarkList; | 
|  | import com.google.devtools.build.lib.syntax.SkylarkType; | 
|  | import com.google.protobuf.TextFormat; | 
|  | import java.io.Serializable; | 
|  | import java.util.ArrayList; | 
|  | import java.util.Collections; | 
|  | import java.util.List; | 
|  | import java.util.Map; | 
|  | import javax.annotation.Nullable; | 
|  |  | 
|  | /** | 
|  | * A generic skylark object with fields, constructable by calling {@code struct()} in skylark. | 
|  | */ | 
|  | public abstract class StructImpl extends Info | 
|  | implements ClassObject, StructApi, Serializable { | 
|  |  | 
|  | /** | 
|  | * Constructs an {@link StructImpl}. | 
|  | * | 
|  | * @param provider the provider describing the type of this instance | 
|  | * @param location the Skylark location where this instance is created. If null, defaults to | 
|  | *     {@link Location#BUILTIN}. | 
|  | */ | 
|  | protected StructImpl(Provider provider, @Nullable Location location) { | 
|  | super(provider, location); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Preprocesses a map of field values to convert the field names and field values to | 
|  | * Skylark-acceptable names and types. | 
|  | * | 
|  | * <p>Entries are ordered by key. | 
|  | */ | 
|  | static ImmutableSortedMap<String, Object> copyValues(Map<String, Object> values) { | 
|  | Preconditions.checkNotNull(values); | 
|  | ImmutableSortedMap.Builder<String, Object> builder = ImmutableSortedMap.naturalOrder(); | 
|  | for (Map.Entry<String, Object> e : values.entrySet()) { | 
|  | builder.put( | 
|  | Attribute.getSkylarkName(e.getKey()), | 
|  | SkylarkType.convertToSkylark(e.getValue(), (Environment) null)); | 
|  | } | 
|  | return builder.build(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns whether the given field name exists. | 
|  | * | 
|  | * <p>This conceptually extends the API for {@link ClassObject}. | 
|  | */ | 
|  | public abstract boolean hasField(String name); | 
|  |  | 
|  | /** | 
|  | * <p>Wraps {@link ClassObject#getValue(String)}, returning null in cases where | 
|  | * {@link EvalException} would have been thrown. | 
|  | */ | 
|  | @VisibleForTesting | 
|  | public Object getValueOrNull(String name) { | 
|  | try { | 
|  | return getValue(name); | 
|  | } catch (EvalException e) { | 
|  | return null; | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns the result of {@link #getValue(String)}, cast as the given type, throwing {@link | 
|  | * EvalException} if the cast fails. | 
|  | */ | 
|  | public <T> T getValue(String key, Class<T> type) throws EvalException { | 
|  | Object obj = getValue(key); | 
|  | if (obj == null) { | 
|  | return null; | 
|  | } | 
|  | SkylarkType.checkType(obj, type, key); | 
|  | return type.cast(obj); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * {@inheritDoc} | 
|  | * | 
|  | * <p>Overrides {@link ClassObject#getFieldNames()}, but does not allow {@link EvalException} to | 
|  | * be thrown. | 
|  | */ | 
|  | @Override | 
|  | public abstract ImmutableCollection<String> getFieldNames(); | 
|  |  | 
|  | /** | 
|  | * Returns the error message format to use for unknown fields. | 
|  | * | 
|  | * <p>By default, it is the one specified by the provider. | 
|  | */ | 
|  | protected String getErrorMessageFormatForUnknownField() { | 
|  | return provider.getErrorMessageFormatForUnknownField(); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public String getErrorMessageForUnknownField(String name) { | 
|  | String suffix = "Available attributes: " | 
|  | + Joiner.on(", ").join(Ordering.natural().sortedCopy(getFieldNames())); | 
|  | return String.format(getErrorMessageFormatForUnknownField(), name) + "\n" + suffix; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public boolean equals(Object otherObject) { | 
|  | if (!(otherObject instanceof StructImpl)) { | 
|  | return false; | 
|  | } | 
|  | StructImpl other = (StructImpl) otherObject; | 
|  | if (this == other) { | 
|  | return true; | 
|  | } | 
|  | if (!this.provider.equals(other.provider)) { | 
|  | return false; | 
|  | } | 
|  | // Compare objects' fields and their values | 
|  | if (!this.getFieldNames().equals(other.getFieldNames())) { | 
|  | return false; | 
|  | } | 
|  | for (String field : getFieldNames()) { | 
|  | if (!Objects.equal(this.getValueOrNull(field), other.getValueOrNull(field))) { | 
|  | return false; | 
|  | } | 
|  | } | 
|  | return true; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public int hashCode() { | 
|  | List<String> fields = new ArrayList<>(getFieldNames()); | 
|  | Collections.sort(fields); | 
|  | List<Object> objectsToHash = new ArrayList<>(); | 
|  | objectsToHash.add(provider); | 
|  | for (String field : fields) { | 
|  | objectsToHash.add(field); | 
|  | objectsToHash.add(getValueOrNull(field)); | 
|  | } | 
|  | return Objects.hashCode(objectsToHash.toArray()); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Convert the object to string using Skylark syntax. The output tries to be reversible (but there | 
|  | * is no guarantee, it depends on the actual values). | 
|  | */ | 
|  | @Override | 
|  | public void repr(SkylarkPrinter printer) { | 
|  | boolean first = true; | 
|  | printer.append("struct("); | 
|  | // Sort by key to ensure deterministic output. | 
|  | for (String fieldName : Ordering.natural().sortedCopy(getFieldNames())) { | 
|  | if (!first) { | 
|  | printer.append(", "); | 
|  | } | 
|  | first = false; | 
|  | printer.append(fieldName); | 
|  | printer.append(" = "); | 
|  | printer.repr(getValueOrNull(fieldName)); | 
|  | } | 
|  | printer.append(")"); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public String toProto(Location loc) throws EvalException { | 
|  | StringBuilder sb = new StringBuilder(); | 
|  | printProtoTextMessage(this, sb, 0, loc); | 
|  | return sb.toString(); | 
|  | } | 
|  |  | 
|  | private void printProtoTextMessage(ClassObject object, StringBuilder sb, int indent, Location loc) | 
|  | throws EvalException { | 
|  | // For determinism sort the fields alphabetically. | 
|  | List<String> fields = new ArrayList<>(object.getFieldNames()); | 
|  | Collections.sort(fields); | 
|  | for (String field : fields) { | 
|  | printProtoTextMessage(field, object.getValue(field), sb, indent, loc); | 
|  | } | 
|  | } | 
|  |  | 
|  | private void printProtoTextMessage( | 
|  | String key, Object value, StringBuilder sb, int indent, Location loc, String container) | 
|  | throws EvalException { | 
|  | if (value instanceof ClassObject) { | 
|  | print(sb, key + " {", indent); | 
|  | printProtoTextMessage((ClassObject) value, sb, indent + 1, loc); | 
|  | print(sb, "}", indent); | 
|  | } else if (value instanceof String) { | 
|  | print( | 
|  | sb, | 
|  | key + ": \"" + escapeDoubleQuotesAndBackslashesAndNewlines((String) value) + "\"", | 
|  | indent); | 
|  | } else if (value instanceof Integer) { | 
|  | print(sb, key + ": " + value, indent); | 
|  | } else if (value instanceof Boolean) { | 
|  | // We're relying on the fact that Java converts Booleans to Strings in the same way | 
|  | // as the protocol buffers do. | 
|  | print(sb, key + ": " + value, indent); | 
|  | } else { | 
|  | throw new EvalException( | 
|  | loc, | 
|  | "Invalid text format, expected a struct, a string, a bool, or an int but got a " | 
|  | + EvalUtils.getDataTypeName(value) | 
|  | + " for " | 
|  | + container | 
|  | + " '" | 
|  | + key | 
|  | + "'"); | 
|  | } | 
|  | } | 
|  |  | 
|  | private void printProtoTextMessage( | 
|  | String key, Object value, StringBuilder sb, int indent, Location loc) throws EvalException { | 
|  | if (value instanceof SkylarkList) { | 
|  | for (Object item : ((SkylarkList) value)) { | 
|  | // TODO(bazel-team): There should be some constraint on the fields of the structs | 
|  | // in the same list but we ignore that for now. | 
|  | printProtoTextMessage(key, item, sb, indent, loc, "list element in struct field"); | 
|  | } | 
|  | } else { | 
|  | printProtoTextMessage(key, value, sb, indent, loc, "struct field"); | 
|  | } | 
|  | } | 
|  |  | 
|  | private void print(StringBuilder sb, String text, int indent) { | 
|  | for (int i = 0; i < indent; i++) { | 
|  | sb.append("  "); | 
|  | } | 
|  | sb.append(text); | 
|  | sb.append("\n"); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Escapes the given string for use in proto/JSON string. | 
|  | * | 
|  | * <p>This escapes double quotes, backslashes, and newlines. | 
|  | */ | 
|  | private static String escapeDoubleQuotesAndBackslashesAndNewlines(String string) { | 
|  | return TextFormat.escapeDoubleQuotesAndBackslashes(string).replace("\n", "\\n"); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public String toJson(Location loc) throws EvalException { | 
|  | StringBuilder sb = new StringBuilder(); | 
|  | printJson(this, sb, loc, "struct field", null); | 
|  | return sb.toString(); | 
|  | } | 
|  |  | 
|  | private void printJson(Object value, StringBuilder sb, Location loc, String container, String key) | 
|  | throws EvalException { | 
|  | if (value == Runtime.NONE) { | 
|  | sb.append("null"); | 
|  | } else if (value instanceof ClassObject) { | 
|  | sb.append("{"); | 
|  |  | 
|  | String join = ""; | 
|  | for (String field : ((ClassObject) value).getFieldNames()) { | 
|  | sb.append(join); | 
|  | join = ","; | 
|  | sb.append("\""); | 
|  | sb.append(field); | 
|  | sb.append("\":"); | 
|  | printJson(((ClassObject) value).getValue(field), sb, loc, "struct field", field); | 
|  | } | 
|  | sb.append("}"); | 
|  | } else if (value instanceof SkylarkDict) { | 
|  | sb.append("{"); | 
|  | String join = ""; | 
|  | for (Map.Entry<?, ?> entry : ((SkylarkDict<?, ?>) value).entrySet()) { | 
|  | sb.append(join); | 
|  | join = ","; | 
|  | if (!(entry.getKey() instanceof String)) { | 
|  | String errorMessage = | 
|  | "Keys must be a string but got a " | 
|  | + EvalUtils.getDataTypeName(entry.getKey()) | 
|  | + " for " | 
|  | + container; | 
|  | if (key != null) { | 
|  | errorMessage += " '" + key + "'"; | 
|  | } | 
|  | throw new EvalException(loc, errorMessage); | 
|  | } | 
|  | sb.append("\""); | 
|  | sb.append(entry.getKey()); | 
|  | sb.append("\":"); | 
|  | printJson(entry.getValue(), sb, loc, "dict value", String.valueOf(entry.getKey())); | 
|  | } | 
|  | sb.append("}"); | 
|  | } else if (value instanceof List) { | 
|  | sb.append("["); | 
|  | String join = ""; | 
|  | for (Object item : ((List) value)) { | 
|  | sb.append(join); | 
|  | join = ","; | 
|  | printJson(item, sb, loc, "list element in struct field", key); | 
|  | } | 
|  | sb.append("]"); | 
|  | } else if (value instanceof String) { | 
|  | sb.append("\""); | 
|  | sb.append(jsonEscapeString((String) value)); | 
|  | sb.append("\""); | 
|  | } else if (value instanceof Integer || value instanceof Boolean) { | 
|  | sb.append(value); | 
|  | } else { | 
|  | String errorMessage = | 
|  | "Invalid text format, expected a struct, a string, a bool, or an int " | 
|  | + "but got a " | 
|  | + EvalUtils.getDataTypeName(value) | 
|  | + " for " | 
|  | + container; | 
|  | if (key != null) { | 
|  | errorMessage += " '" + key + "'"; | 
|  | } | 
|  | throw new EvalException(loc, errorMessage); | 
|  | } | 
|  | } | 
|  |  | 
|  | private String jsonEscapeString(String string) { | 
|  | return escapeDoubleQuotesAndBackslashesAndNewlines(string) | 
|  | .replace("\r", "\\r") | 
|  | .replace("\t", "\\t"); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public String toString() { | 
|  | return Printer.repr(this); | 
|  | } | 
|  | } |