blob: e525fc6b7cdfb816462ee0b17a284d6bdbb8eae1 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.brooklyn.core.objs;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Map;
import com.google.common.annotations.Beta;
import com.google.common.base.Function;
import com.google.common.base.Objects;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.reflect.TypeToken;
import org.apache.brooklyn.api.catalog.CatalogConfig;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.entity.EntitySpec;
import org.apache.brooklyn.api.internal.AbstractBrooklynObjectSpec;
import org.apache.brooklyn.api.location.PortRange;
import org.apache.brooklyn.api.mgmt.ManagementContext;
import org.apache.brooklyn.api.mgmt.classloading.BrooklynClassLoadingContext;
import org.apache.brooklyn.api.objs.BrooklynObject;
import org.apache.brooklyn.api.objs.BrooklynType;
import org.apache.brooklyn.api.objs.SpecParameter;
import org.apache.brooklyn.api.sensor.AttributeSensor;
import org.apache.brooklyn.config.ConfigInheritance;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.config.ConfigKey.HasConfigKey;
import org.apache.brooklyn.core.config.BasicConfigKey;
import org.apache.brooklyn.core.config.BasicConfigKey.Builder;
import org.apache.brooklyn.core.sensor.PortAttributeSensorAndConfigKey;
import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.guava.Maybe;
import org.apache.brooklyn.util.text.StringPredicates;
import org.apache.brooklyn.util.time.Duration;
public class BasicSpecParameter<T> implements SpecParameter<T>{
private static final long serialVersionUID = -4728186276307619778L;
private final String label;
/** pinning may become a priority or other more expansive indicator */
@Beta
private final boolean pinned;
private final ConfigKey<T> configKey;
private final AttributeSensor<?> sensor;
// For backwards compatibility of persisted state.
// Automatically called by xstream (which is used under the covers by XmlMementoSerializer).
// Required for those who have state from a version between
// 29th October 2015 and 21st January 2016 (when this class was introduced, and then when it was changed).
private ConfigKey<T> type;
private Object readResolve() {
if (type != null && configKey == null) {
return new BasicSpecParameter(label, pinned, type, sensor);
} else {
return this;
}
}
@Beta // TBD whether "pinned" stays
public BasicSpecParameter(String label, boolean pinned, ConfigKey<T> config) {
this(label, pinned, config, null);
}
@Beta // TBD whether "pinned" and "sensor" stay
public <SensorType> BasicSpecParameter(String label, boolean pinned, ConfigKey<T> config, AttributeSensor<SensorType> sensor) {
this.label = label;
this.pinned = pinned;
this.configKey = config;
this.sensor = sensor;
}
@Override
public String getLabel() {
return label;
}
@Override
public boolean isPinned() {
return pinned;
}
@Override
public ConfigKey<T> getConfigKey() {
return configKey;
}
@Override
public AttributeSensor<?> getSensor() {
return sensor;
}
@Override
public int hashCode() {
return Objects.hashCode(label, pinned, configKey);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
BasicSpecParameter<?> other = (BasicSpecParameter<?>) obj;
return Objects.equal(configKey, other.configKey);
}
@Override
public String toString() {
return Objects.toStringHelper(this)
.add("label", label)
.add("pinned", pinned)
.add("type", configKey)
.toString();
}
// Not CAMP specific since it's used to get the parameters from catalog items meta which are syntax independent.
public static List<SpecParameter<?>> fromConfigList(List<?> obj, BrooklynClassLoadingContext loader) {
return ParseYamlInputs.parseParameters(obj, loader);
}
public static List<SpecParameter<?>> fromClass(ManagementContext mgmt, Class<?> type) {
return ParseClassParameters.parseParameters(getImplementedBy(mgmt, type));
}
public static List<SpecParameter<?>> fromSpec(ManagementContext mgmt, AbstractBrooklynObjectSpec<?, ?> spec) {
if (!spec.getParameters().isEmpty()) {
return spec.getParameters();
}
Class<?> type = null;
if (spec instanceof EntitySpec) {
EntitySpec<?> entitySpec = (EntitySpec<?>)spec;
if (entitySpec.getImplementation() != null) {
type = entitySpec.getImplementation();
}
}
if (type == null) {
type = getImplementedBy(mgmt, spec.getType());
}
return ParseClassParameters.parseParameters(getImplementedBy(mgmt, type));
}
private static Class<?> getImplementedBy(ManagementContext mgmt, Class<?> type) {
if (Entity.class.isAssignableFrom(type) && type.isInterface()) {
try {
@SuppressWarnings("unchecked")
Class<? extends Entity> uncheckedType = ParseClassParameters.getEntityImplementedBy(mgmt, (Class<Entity>)type);
return uncheckedType;
} catch(IllegalArgumentException e) {
// NOP, no implementation for type, use whatever we have
}
}
return type;
}
private static final class ParseYamlInputs {
private static final String DEFAULT_TYPE = "string";
private static final Map<String, Class<?>> BUILT_IN_TYPES = ImmutableMap.<String, Class<?>>builder()
.put(DEFAULT_TYPE, String.class)
.put("boolean", Boolean.class)
.put("byte", Byte.class)
.put("char", Character.class)
.put("short", Short.class)
.put("integer", Integer.class)
.put("long", Long.class)
.put("float", Float.class)
.put("double", Double.class)
.put("duration", Duration.class)
.put("timestamp", Date.class)
.put("port", PortRange.class)
.build();
private static final Map<String, Predicate<?>> BUILT_IN_CONSTRAINTS = ImmutableMap.<String, Predicate<?>>of(
"required", StringPredicates.isNonBlank());
public static List<SpecParameter<?>> parseParameters(List<?> inputsRaw, BrooklynClassLoadingContext loader) {
if (inputsRaw == null) return ImmutableList.of();
List<SpecParameter<?>> inputs = new ArrayList<>(inputsRaw.size());
for (Object obj : inputsRaw) {
inputs.add(parseParameter(obj, loader));
}
return inputs;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private static SpecParameter<?> parseParameter(Object obj, BrooklynClassLoadingContext loader) {
Map inputDef;
if (obj instanceof String) {
inputDef = ImmutableMap.of("name", obj);
} else if (obj instanceof Map) {
inputDef = (Map) obj;
} else {
throw new IllegalArgumentException("Catalog input definition expected to be a map, but is " + obj.getClass() + " instead: " + obj);
}
String name = (String)inputDef.get("name");
String label = (String)inputDef.get("label");
String description = (String)inputDef.get("description");
String type = (String)inputDef.get("type");
Object defaultValue = inputDef.get("default");
Predicate<?> constraints = parseConstraints(inputDef.get("constraints"), loader);
ConfigInheritance parentInheritance = parseInheritance(inputDef.get("inheritance.parent"), loader);
ConfigInheritance typeInheritance = parseInheritance(inputDef.get("inheritance.type"), loader);
if (name == null) {
throw new IllegalArgumentException("'name' value missing from input definition " + obj + " but is required. Check for typos.");
}
ConfigKey configType;
AttributeSensor sensorType = null;
TypeToken typeToken = inferType(type, loader);
Builder builder = BasicConfigKey.builder(typeToken)
.name(name)
.description(description)
.defaultValue(defaultValue)
.constraint(constraints)
.parentInheritance(parentInheritance)
.typeInheritance(typeInheritance);
if (PortRange.class.equals(typeToken.getRawType())) {
sensorType = new PortAttributeSensorAndConfigKey(builder);
configType = ((HasConfigKey)sensorType).getConfigKey();
} else {
configType = builder.build();
}
return new BasicSpecParameter(Objects.firstNonNull(label, name), true, configType, sensorType);
}
@SuppressWarnings({ "rawtypes" })
private static TypeToken inferType(String typeRaw, BrooklynClassLoadingContext loader) {
if (typeRaw == null) return TypeToken.of(String.class);
String type = typeRaw.trim();
if (BUILT_IN_TYPES.containsKey(type)) {
return TypeToken.of(BUILT_IN_TYPES.get(type));
} else {
// Assume it's a Java type
Maybe<Class<?>> inputType = loader.tryLoadClass(type);
if (inputType.isPresent()) {
return TypeToken.of(inputType.get());
} else {
throw new IllegalArgumentException("The type '" + type + "' for a catalog input not recognised as a built-in (" + BUILT_IN_TYPES.keySet() + ") or a java type");
}
}
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private static Predicate parseConstraints(Object obj, BrooklynClassLoadingContext loader) {
List constraintsRaw;
if (obj == null) {
constraintsRaw = ImmutableList.of();
} else if (obj instanceof String) {
constraintsRaw = ImmutableList.of(obj);
} else if (obj instanceof List) {
constraintsRaw = (List) obj;
} else {
throw new IllegalArgumentException ("The constraint '" + obj + "' for a catalog input is invalid format - string or list supported");
}
List<Predicate> constraints = new ArrayList(constraintsRaw.size());
for (Object untypedConstraint : constraintsRaw) {
String constraint = (String)untypedConstraint;
if (BUILT_IN_CONSTRAINTS.containsKey(constraint)) {
constraints.add(BUILT_IN_CONSTRAINTS.get(constraint));
} else {
throw new IllegalArgumentException("The constraint '" + constraint + "' for a catalog input is not recognized as a built-in (" + BUILT_IN_CONSTRAINTS.keySet() + ")");
}
}
if (!constraints.isEmpty()) {
if (constraints.size() == 1) {
return constraints.get(0);
} else {
return Predicates.and((List<Predicate<Object>>)(List) constraints);
}
} else {
return Predicates.alwaysTrue();
}
}
private static ConfigInheritance parseInheritance(Object obj, BrooklynClassLoadingContext loader) {
if (obj == null || obj instanceof String) {
return ConfigInheritance.fromString((String)obj);
} else {
throw new IllegalArgumentException ("The config-inheritance '" + obj + "' for a catalog input is invalid format - string supported");
}
}
}
private static final class ParseClassParameters {
private static final class WeightedParameter {
private Double weight;
private SpecParameter<?> input;
public WeightedParameter(Double weight, SpecParameter<?> input) {
this.weight = weight;
this.input = input;
}
public Double getWeight() {return weight; }
public SpecParameter<?> getInput() { return input; }
}
private static final class WeightedParameterComparator implements Comparator<WeightedParameter> {
@Override
public int compare(WeightedParameter o1, WeightedParameter o2) {
if (o1.getWeight() == null && o2.getWeight() == null) {
return o1.getInput().getLabel().compareTo(o2.getInput().getLabel());
} else if (o1.getWeight() == null) {
return 1;
} else if (o2.getWeight() == null) {
return -1;
} else {
return -Double.compare(o1.getWeight(), o2.getWeight());
}
}
}
private static final class SpecParameterTransformer implements Function<WeightedParameter, SpecParameter<?>> {
@Override
public SpecParameter<?> apply(WeightedParameter input) {
return input.getInput();
}
}
public static List<SpecParameter<?>> parseParameters(Class<?> c) {
MutableList<WeightedParameter> parameters = MutableList.<WeightedParameter>of();
if (BrooklynObject.class.isAssignableFrom(c)) {
@SuppressWarnings("unchecked")
Class<? extends BrooklynObject> brooklynClass = (Class<? extends BrooklynObject>) c;
BrooklynDynamicType<?, ?> dynamicType = BrooklynTypes.getDefinedBrooklynType(brooklynClass);
BrooklynType type = dynamicType.getSnapshot();
for (ConfigKey<?> x: type.getConfigKeys()) {
WeightedParameter fieldConfig = getFieldConfig(x, dynamicType.getConfigKeyField(x.getName()));
parameters.appendIfNotNull(fieldConfig);
}
Collections.sort(parameters, new WeightedParameterComparator());
return FluentIterable.from(parameters)
.transform(new SpecParameterTransformer()).toList();
} else {
return ImmutableList.of();
}
}
public static WeightedParameter getFieldConfig(ConfigKey<?> config, Field configKeyField) {
if (configKeyField == null) return null;
CatalogConfig catalogConfig = configKeyField.getAnnotation(CatalogConfig.class);
String label = config.getName();
Double priority = null;
if (catalogConfig != null) {
label = Maybe.fromNullable(catalogConfig.label()).or(config.getName());
priority = catalogConfig.priority();
}
@SuppressWarnings({ "unchecked", "rawtypes" })
SpecParameter<?> param = new BasicSpecParameter(label, priority != null, config);
return new WeightedParameter(priority, param);
}
private static <T extends Entity> Class<? extends T> getEntityImplementedBy(ManagementContext mgmt, Class<T> type) {
return mgmt.getEntityManager().getEntityTypeRegistry().getImplementedBy(type);
}
}
/**
* Adds the given list of {@link SpecParameter parameters} to the provided
* {@link AbstractBrooklynObjectSpec spec} or generates a list from the
* spec if the provided list is empty.
*
* @see EntitySpec#parameters(List)
*/
public static void addParameters(AbstractBrooklynObjectSpec<?, ?> spec, List<? extends SpecParameter<?>> explicitParams, BrooklynClassLoadingContext loader) {
if (spec.getParameters().isEmpty()) {
spec.parametersAdd(BasicSpecParameter.fromSpec(loader.getManagementContext(), spec));
}
if (explicitParams.size() > 0) {
spec.parametersAdd(explicitParams);
}
}
}