com.google.api.tools.framework.aspects.Feature 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.aspects;
import com.google.api.tools.framework.model.Diag;
import com.google.api.tools.framework.model.DiagCollector;
import com.google.api.tools.framework.model.Location;
import com.google.common.base.Preconditions;
import com.google.common.collect.DiscreteDomain;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Range;
import java.util.List;
import java.util.Set;
/**
* Feature represents a platform behavior that may not be suitable for all services. For example,
* a feature may control whether an API proxy should automatically serve API description (discovery)
* documents.
*
* A feature:
*
* - has a name composed of '/' separated parts, starting with a namespace identifier
* - may be enabled or disabled by default at a particular config version
* - only be available at a limited range of config versions
* - may have child features that are enabled when the parent is enabled
*
*/
public class Feature {
private final String featureName;
private final List childBindings;
private final Range supportedVersionRange;
private final Range defaultVersionRange;
private Feature(Builder builder) {
Preconditions.checkArgument(
!builder.supportedVersionRange.canonical(DiscreteDomain.integers()).isEmpty(),
"supportedVersionRange must not be empty");
featureName = builder.featureName;
childBindings = builder.childBindings.build();
supportedVersionRange = builder.supportedVersionRange;
defaultVersionRange = builder.defaultVersionRange;
}
/**
* Returns true if the feature is enabled by default in the given config version.
*/
boolean isDefaultIn(int configVersion) {
return defaultVersionRange.contains(configVersion);
}
/**
* Returns true if the feature is supported in the given config version.
*/
boolean isSupportedIn(int configVersion) {
return supportedVersionRange.contains(configVersion);
}
/**
* Adds or removes this feature from the given set of enabled features. Validates that this
* feature is available in the given config version.
*
* If this feature has children bound at the given config version, then those are accumulated
* as well.
*/
void accumulate(Set enabled, boolean remove,
int configVersion, DiagCollector diags, Location location) {
if (!remove && !isSupportedIn(configVersion)) {
diags.addDiag(Diag.error(location, "Feature %s not available in config version %s.",
this, configVersion));
}
if (!remove) {
if (!enabled.add(featureName)) {
diags.addDiag(Diag.warning(location,
"Enabling feature %s had no effect because the feature was already enabled.", this));
}
} else {
if (!enabled.remove(featureName)) {
diags.addDiag(Diag.warning(location,
"Disabling feature %s had no effect because the feature was already disabled.", this));
}
}
for (ChildBinding binding : childBindings) {
if (binding.range.contains(configVersion)) {
binding.child.accumulate(enabled, remove, configVersion, diags, location);
}
}
}
/**
* Retrieve all child names. (For validation in Features).
*/
Iterable getChildNames() {
ImmutableList.Builder names = ImmutableList.builder();
for (ChildBinding binding : childBindings) {
names.add(binding.child.getName());
}
return names.build();
}
void addSelfAndChildren(ImmutableMap.Builder featuresByNameBuilder) {
featuresByNameBuilder.put(featureName, this);
for (ChildBinding binding : childBindings) {
binding.child.addSelfAndChildren(featuresByNameBuilder);
}
}
/**
* Gets the name of the feature.
*/
String getName() {
return featureName;
}
@Override
public String toString() {
return featureName;
}
/**
* Creates a new Builder with the given feature name.
*/
public static Builder builder(String featureName) {
return new Builder(featureName);
}
private static class ChildBinding {
private final Feature child;
private final Range range;
ChildBinding(Feature child, Range range) {
this.child = child;
this.range = range;
}
}
/**
* Builder for {@link Feature}s.
*/
public static class Builder {
private final String featureName;
private Range supportedVersionRange = Range.all();
private Range defaultVersionRange = Range.openClosed(0, 0); // Empty
private ImmutableList.Builder childBindings = ImmutableList.builder();
private Builder(String featureName) {
Preconditions.checkNotNull(featureName);
this.featureName = featureName;
}
/**
* Sets the range of versions where this feature is supported.
*/
public Builder withSupportedRange(Range versionRange) {
Preconditions.checkNotNull(versionRange);
supportedVersionRange = versionRange;
return this;
}
/**
* Sets the range of versions where this feature is enabled by default.
*/
public Builder withDefaultRange(Range versionRange) {
Preconditions.checkNotNull(versionRange);
defaultVersionRange = versionRange;
return this;
}
/**
* Automatically includes the given child when the config version is in the given range.
*/
public Builder withChild(Feature childFeature, Range versionRange) {
Preconditions.checkNotNull(childFeature);
Preconditions.checkNotNull(versionRange);
Preconditions.checkArgument(childFeature.getName().startsWith(featureName + "/"),
"Child feature name %s must start with parent feature name %s + '/'",
childFeature.getName(), featureName);
String childPart = childFeature.getName().substring(featureName.length() + 1);
Preconditions.checkArgument(
childPart.indexOf('/') == -1, "Child feature name must not contain '/'");
childBindings.add(new ChildBinding(childFeature, versionRange));
return this;
}
private static void checkEncloses(
Range outer, Range inner, String format, Object... additionalArgs) {
Object[] formatArgs = new Object[additionalArgs.length + 2];
formatArgs[0] = outer;
formatArgs[1] = inner;
System.arraycopy(additionalArgs, 0, formatArgs, 2, additionalArgs.length);
Preconditions.checkArgument(outer.encloses(inner) || inner.isEmpty(), format, formatArgs);
}
/**
* Returns the feature with the previously given configuration.
*/
public Feature build() {
checkEncloses(supportedVersionRange, defaultVersionRange,
"supportedVersionRange %s does not enclose defaultVersionRange %s on %s", featureName);
for (ChildBinding binding : childBindings.build()) {
checkEncloses(supportedVersionRange, binding.range,
"parent supportedVersionRange %s does not include binding range %s for child %s",
binding.child.featureName);
checkEncloses(binding.child.supportedVersionRange, binding.range,
"child supportedVersionRange %s does not include binding range %s for child %s",
binding.child.featureName);
}
return new Feature(this);
}
}
}