com.google.api.tools.framework.model.ConfigSource Maven / Gradle / Ivy
/*
* Copyright (C) 2016 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.api.tools.framework.model;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.protobuf.Descriptors.Descriptor;
import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.MapEntry;
import com.google.protobuf.Message;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import javax.annotation.Nullable;
/**
* Represents a configuration source. Maintains information about the configuration message, the
* fields which have been explicitly set in the configuration, and source locations. Also supports
* merging of configurations preserving above information, and allowing to override default values
* (which standard proto3 merging semantics does not support).
*/
public class ConfigSource implements ConfigLocationResolver {
// Contains the config message.
private final Message configMessage;
// Contains a map from message keys into a map of location keys with associated location.
//
// A message key is a wrapper around a message which uses the messages's object identity
// for equality. This way, we can work around that by default, messages have value equality,
// and effectively can attach attributes to the tree of messages represented by configMessage.
// The attribute in this case is the map from location key into source location, for a given
// message. A location key consists of a field descriptor, and an optional key for a map or
// index into a list.
//
// The location map is leveraged for merging. Presence of a location key indicates that an update
// has been performed. This is used to support proto2 semantics for merging.
private final ImmutableMap> locations;
private ConfigSource(
Message value, ImmutableMap> locations) {
this.configMessage = value;
this.locations = locations;
}
/** Returns the config message. */
public Message getConfig() {
return configMessage;
}
/**
* Returns the service config file location of the given named field in the (sub)message. Returns
* {@link SimpleLocation#TOPLEVEL} if the location is not known.
*/
@Override
public Location getLocationInConfig(Message message, String fieldName) {
Location loc = getLocation(message, fieldName, null);
return loc != SimpleLocation.UNKNOWN ? loc : SimpleLocation.TOPLEVEL;
}
/**
* Returns the service config file location of the given named field in the (sub)message. The key
* identifies the key of the map. For repeated fields, the element key is a zero-based index.
* Returns {@link SimpleLocation#TOPLEVEL} if the location is not known.
*/
@Override
public Location getLocationOfRepeatedFieldInConfig(
Message message, String fieldName, Object elementKey) {
Location loc = getLocation(message, fieldName, elementKey);
return loc != SimpleLocation.UNKNOWN ? loc : SimpleLocation.TOPLEVEL;
}
/**
* Returns the location of the given named field in the (sub)message, with optional element key
* for maps or repeated fields. For repeated fields, the element key is a zero-based index.
* Returns {@link SimpleLocation#UNKNOWN} if the location is not known.
*/
public Location getLocation(Message message, String fieldName, @Nullable Object elementKey) {
MessageKey messageKey = new MessageKey(message);
Map map = locations.get(messageKey);
if (map != null) {
FieldDescriptor field = message.getDescriptorForType().findFieldByName(fieldName);
if (field != null) {
Location result = map.get(new LocationKey(field, elementKey));
if (result != null) {
return result;
}
}
}
return SimpleLocation.UNKNOWN;
}
/** Constructs a builder from this configuration message. */
public Builder toBuilder() {
return new Builder(configMessage, configMessage.toBuilder(), new LinkedHashMap<>(locations));
}
/**
* Constructs a new empty builder, based on the given default instance for the underlying config
* message.
*
* An initialized message can also be passed, however, no source location tracking will happen
* for it.
*/
public static Builder newBuilder(Message defaultInstance) {
return new Builder(
defaultInstance,
defaultInstance.toBuilder(),
new LinkedHashMap>());
}
/**
* An interface which represents a build action performed on a builder for a sub-configuration.
*/
// In Java 8 this could be represented by the Consumer interface, but we cannot depend
// on this.
public interface BuildAction {
public void accept(Builder builder);
}
/** Represents a builder for a configuration message. */
public static class Builder {
// The message from which we build. For a fresh builder, this is the default instance.
private final Message configMessage;
// The builder for the message.
private final Message.Builder configBuilder;
// The locations map for the entire built tree. This is shared with parents, and updates
// to here are global.
private final Map> locations;
// New locations added to this builder.
private final Map newLocations = new LinkedHashMap<>();
// Whether build() was called.
private boolean built;
private Builder(
Message message,
Message.Builder messageBuilder,
Map> locations) {
this.configMessage = message;
this.configBuilder = messageBuilder;
this.locations = locations;
}
/** Return the descriptor for the message being built. */
public Descriptor getDescriptorForType() {
return configBuilder.getDescriptorForType();
}
public ConfigSource build() {
Preconditions.checkState(!built, "Called build twice on config source");
built = true;
// Build value.
Message newMessage = configBuilder.build();
// Propagate locations from the old version of the message in the new one which is built.
Map oldLocations = locations.remove(new MessageKey(configMessage));
if (oldLocations != null) {
for (LocationKey key : oldLocations.keySet()) {
if (!newLocations.containsKey(key)) {
newLocations.put(key, oldLocations.get(key));
}
}
}
// Update locations for new message.
if (!newLocations.isEmpty()) {
locations.put(new MessageKey(newMessage), ImmutableMap.copyOf(newLocations));
}
return new ConfigSource(newMessage, ImmutableMap.copyOf(locations));
}
/**
* Sets the given scalar value on the field. If optional key is provided, the field must
* represent a map, and the value under the key is set.
*/
public Builder setValue(
FieldDescriptor field, @Nullable Object key, Object value, @Nullable Location location) {
if (key == null) {
configBuilder.setField(field, value);
} else {
putMapEntry(configBuilder, field, key, value);
}
addLocation(field, key, location);
return this;
}
public void addLocation(FieldDescriptor field, Object key, Location location) {
newLocations.put(new LocationKey(field, key), nonNull(location));
}
/** Adds the scalar value to the field which must be repeated. */
public Builder addValue(FieldDescriptor field, Object value, @Nullable Location location) {
int index = configBuilder.getRepeatedFieldCount(field);
configBuilder.addRepeatedField(field, value);
newLocations.put(new LocationKey(field, index), nonNull(location));
return this;
}
/**
* Constructs a sub-builder for given field and calls the action on it. After the action's
* processing, the sub-message will be build and stored into the field of this builder.
* Moreover, update locations of the sub-builder are integrated into this builder.
*/
public Builder withBuilder(FieldDescriptor field, BuildAction action) {
// Construct a fresh builder for the given field and merge in the current value. As we depend
// on message identity, we need to control this builder directly, so can't use implicit
// building via getFieldBuilder. A builder created by getFieldBuilder as build() called
// when the parent is called, resulting in a different message identity.
Message currentValue = (Message) configBuilder.getField(field);
Message.Builder protoBuilder = configBuilder.newBuilderForField(field);
protoBuilder.mergeFrom(currentValue);
// Construct config builder, and let the action process it.
Builder fieldConfigBuilder = new Builder(currentValue, protoBuilder, locations);
action.accept(fieldConfigBuilder);
// Build config, which updates the location mapping, and update proto builder.
ConfigSource fieldConfig = fieldConfigBuilder.build();
configBuilder.setField(field, fieldConfig.getConfig());
return this;
}
/**
* Constructs a sub-builder for given field and calls the action on it. If optional key is
* provided, the field must represent a map of messages, and the builder under the key is used.
*/
public Builder withBuilder(FieldDescriptor field, @Nullable Object key, BuildAction action) {
// If there is no key, behave like the similar method without key.
if (key == null) {
return withBuilder(field, action);
}
// The reflection API for maps is rather incomplete, so we need to hack.
// First get the map for the underlying field, and determine the field
// for the map entry's value.
Map