/*
* 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.typereg;
import java.lang.reflect.Method;
import java.util.Comparator;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
import org.apache.brooklyn.api.catalog.CatalogItem;
import org.apache.brooklyn.api.internal.AbstractBrooklynObjectSpec;
import org.apache.brooklyn.api.mgmt.ManagementContext;
import org.apache.brooklyn.api.objs.BrooklynObject;
import org.apache.brooklyn.api.typereg.BrooklynTypeRegistry.RegisteredTypeKind;
import org.apache.brooklyn.api.typereg.RegisteredType;
import org.apache.brooklyn.api.typereg.RegisteredType.TypeImplementationPlan;
import org.apache.brooklyn.api.typereg.RegisteredTypeLoadingContext;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.core.catalog.internal.CatalogUtils;
import org.apache.brooklyn.core.config.ConfigKeys;
import org.apache.brooklyn.core.objs.BrooklynObjectInternal;
import org.apache.brooklyn.core.typereg.JavaClassNameTypePlanTransformer.JavaClassNameTypeImplementationPlan;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.guava.Maybe;
import org.apache.brooklyn.util.guava.Maybe.Absent;
import org.apache.brooklyn.util.text.NaturalOrderComparator;
import org.apache.brooklyn.util.text.VersionComparator;
import org.apache.brooklyn.util.yaml.Yamls;
import com.google.common.annotations.Beta;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.Ordering;
import com.google.common.reflect.TypeToken;
/**
* Utility and preferred creation mechanisms for working with {@link RegisteredType} instances.
* <p>
* Use {@link #bean(String, String, TypeImplementationPlan, Class)} and {@link #spec(String, String, TypeImplementationPlan, Class)}
* to create {@link RegisteredType} instances.
* <p>
* See {@link #isSubtypeOf(RegisteredType, Class)} or {@link #isSubtypeOf(RegisteredType, RegisteredType)} to
* inspect the type hierarchy.
*/
public class RegisteredTypes {
@SuppressWarnings("serial")
static ConfigKey<Class<?>> ACTUAL_JAVA_TYPE = ConfigKeys.newConfigKey(new TypeToken<Class<?>>() {}, "java.type.actual",
"The actual Java type which will be instantiated (bean) or pointed at (spec)");
/** @deprecated since it was introduced in 0.9.0; for backwards compatibility only, may be removed at any point */
@Deprecated
static final Function<CatalogItem<?,?>,RegisteredType> CI_TO_RT = new Function<CatalogItem<?,?>, RegisteredType>() {
@Override
public RegisteredType apply(CatalogItem<?, ?> item) {
return of(item);
}
};
/** @deprecated since it was introduced in 0.9.0; for backwards compatibility only, may be removed at any point */
@Deprecated
public static RegisteredType of(CatalogItem<?, ?> item) {
if (item==null) return null;
TypeImplementationPlan impl = null;
if (item.getPlanYaml()!=null) {
impl = new BasicTypeImplementationPlan(null, item.getPlanYaml());
} else if (item.getJavaType()!=null) {
impl = new JavaClassNameTypeImplementationPlan(item.getJavaType());
} else {
throw new IllegalStateException("Unsupported catalog item "+item+" when trying to create RegisteredType");
}
BasicRegisteredType type = (BasicRegisteredType) spec(item.getSymbolicName(), item.getVersion(), impl, item.getCatalogItemJavaType());
type.displayName = item.getDisplayName();
type.description = item.getDescription();
type.iconUrl = item.getIconUrl();
type.disabled = item.isDisabled();
type.deprecated = item.isDeprecated();
if (item.getLibraries()!=null) type.bundles.addAll(item.getLibraries());
// aliases aren't on item
if (item.tags()!=null) type.tags.addAll(item.tags().getTags());
// these things from item we ignore: javaType, specType, registeredTypeName ...
return type;
}
/** Preferred mechanism for defining a bean {@link RegisteredType}. */
public static RegisteredType bean(String symbolicName, String version, TypeImplementationPlan plan, @Nullable Class<?> superType) {
return addSuperType(new BasicRegisteredType(RegisteredTypeKind.BEAN, symbolicName, version, plan), superType);
}
/** Preferred mechanism for defining a spec {@link RegisteredType}. */
// TODO we currently allow symbolicName and version to be null for the purposes of creation, internal only in BasicBrooklynTypeRegistry.createSpec
// (ideally the API in TypePlanTransformer can be changed so even that is not needed)
public static RegisteredType spec(String symbolicName, String version, TypeImplementationPlan plan, @Nullable Class<?> superType) {
return addSuperType(new BasicRegisteredType(RegisteredTypeKind.SPEC, symbolicName, version, plan), superType);
}
/** returns the {@link Class} object corresponding to the given java type name,
* using the cache on the type and the loader defined on the type
* @param mgmt */
@Beta
// TODO should this be on the AbstractTypePlanTransformer ?
public static Class<?> loadActualJavaType(String javaTypeName, ManagementContext mgmt, RegisteredType type, RegisteredTypeLoadingContext context) {
Class<?> result = ((BasicRegisteredType)type).getCache().get(ACTUAL_JAVA_TYPE);
if (result!=null) return result;
result = CatalogUtils.newClassLoadingContext(mgmt, type, context==null ? null : context.getLoader()).loadClass( javaTypeName );
((BasicRegisteredType)type).getCache().put(ACTUAL_JAVA_TYPE, result);
return result;
}
@Beta
public static RegisteredType addSuperType(RegisteredType type, @Nullable Class<?> superType) {
if (superType!=null) {
((BasicRegisteredType)type).superTypes.add(superType);
}
return type;
}
@Beta
public static RegisteredType addSuperType(RegisteredType type, @Nullable RegisteredType superType) {
if (superType!=null) {
if (isSubtypeOf(superType, type)) {
throw new IllegalStateException(superType+" declares "+type+" as a supertype; cannot set "+superType+" as a supertype of "+type);
}
((BasicRegisteredType)type).superTypes.add(superType);
}
return type;
}
@Beta
public static RegisteredType addSuperTypes(RegisteredType type, Iterable<Object> superTypesAsClassOrRegisteredType) {
if (superTypesAsClassOrRegisteredType!=null) {
for (Object superType: superTypesAsClassOrRegisteredType) {
if (superType==null) {
// nothing
} else if (superType instanceof Class) {
addSuperType(type, (Class<?>)superType);
} else if (superType instanceof RegisteredType) {
addSuperType(type, (RegisteredType)superType);
} else {
throw new IllegalStateException(superType+" supplied as a supertype of "+type+" but it is not a supported supertype");
}
}
}
return type;
}
@Beta
public static RegisteredType addAlias(RegisteredType type, String alias) {
if (alias!=null) {
((BasicRegisteredType)type).aliases.add( alias );
}
return type;
}
@Beta
public static RegisteredType addAliases(RegisteredType type, Iterable<String> aliases) {
if (aliases!=null) {
for (String alias: aliases) addAlias(type, alias);
}
return type;
}
@Beta
public static RegisteredType addTag(RegisteredType type, Object tag) {
if (tag!=null) {
((BasicRegisteredType)type).tags.add( tag );
}
return type;
}
@Beta
public static RegisteredType addTags(RegisteredType type, Iterable<?> tags) {
if (tags!=null) {
for (Object tag: tags) addTag(type, tag);
}
return type;
}
/** returns the implementation data for a spec if it is a string (e.g. plan yaml or java class name); else throws */
@Beta
public static String getImplementationDataStringForSpec(RegisteredType item) {
if (item==null || item.getPlan()==null) return null;
Object data = item.getPlan().getPlanData();
if (!(data instanceof String)) throw new IllegalStateException("Expected plan data for "+item+" to be a string");
return (String)data;
}
/** returns an implementation of the spec class corresponding to the given target type;
* for use in {@link BrooklynTypePlanTransformer#create(RegisteredType, RegisteredTypeLoadingContext)}
* implementations when dealing with a spec; returns null if none found
* @param mgmt */
@Beta
public static AbstractBrooklynObjectSpec<?,?> newSpecInstance(ManagementContext mgmt, Class<? extends BrooklynObject> targetType) throws Exception {
Class<? extends AbstractBrooklynObjectSpec<?, ?>> specType = RegisteredTypeLoadingContexts.lookupSpecTypeForTarget(targetType);
if (specType==null) return null;
Method createMethod = specType.getMethod("create", Class.class);
return (AbstractBrooklynObjectSpec<?, ?>) createMethod.invoke(null, targetType);
}
/** Returns a wrapped map, if the object is YAML which parses as a map;
* otherwise returns absent capable of throwing an error with more details */
@SuppressWarnings("unchecked")
public static Maybe<Map<?,?>> getAsYamlMap(Object planData) {
if (!(planData instanceof String)) return Maybe.absent("not a string");
Iterable<Object> result;
try {
result = Yamls.parseAll((String)planData);
} catch (Exception e) {
Exceptions.propagateIfFatal(e);
return Maybe.absent(e);
}
Iterator<Object> ri = result.iterator();
if (!ri.hasNext()) return Maybe.absent("YAML has no elements in it");
Object r1 = ri.next();
if (ri.hasNext()) return Maybe.absent("YAML has multiple elements in it");
if (r1 instanceof Map) return (Maybe<Map<?,?>>)(Maybe<?>) Maybe.of(r1);
return Maybe.absent("YAML does not contain a map");
}
/**
* Queries recursively the supertypes of {@link RegisteredType} to see whether it
* inherits from the given {@link RegisteredType} */
public static boolean isSubtypeOf(RegisteredType type, RegisteredType superType) {
if (type.equals(superType)) return true;
for (Object st: type.getSuperTypes()) {
if (st instanceof RegisteredType) {
if (isSubtypeOf((RegisteredType)st, superType)) return true;
}
}
return false;
}
/**
* Queries recursively the supertypes of {@link RegisteredType} to see whether it
* inherits from the given {@link Class} */
public static boolean isSubtypeOf(RegisteredType type, Class<?> superType) {
return isAnyTypeSubtypeOf(type.getSuperTypes(), superType);
}
/**
* Queries recursively the given types (either {@link Class} or {@link RegisteredType})
* to see whether any inherit from the given {@link Class} */
public static boolean isAnyTypeSubtypeOf(Set<Object> candidateTypes, Class<?> superType) {
return isAnyTypeOrSuperSatisfying(candidateTypes, Predicates.assignableFrom(superType));
}
/**
* Queries recursively the given types (either {@link Class} or {@link RegisteredType})
* to see whether any java superclasses satisfy the given {@link Predicate} */
public static boolean isAnyTypeOrSuperSatisfying(Set<Object> candidateTypes, Predicate<Class<?>> filter) {
for (Object st: candidateTypes) {
if (st instanceof Class) {
if (filter.apply((Class<?>)st)) return true;
}
}
for (Object st: candidateTypes) {
if (st instanceof RegisteredType) {
if (isAnyTypeOrSuperSatisfying(((RegisteredType)st).getSuperTypes(), filter)) return true;
}
}
return false;
}
/** Validates that the given type matches the context (if supplied);
* if not satisfied. returns an {@link Absent} if failed with details of the error,
* with {@link Absent#isNull()} true if the object is null. */
public static Maybe<RegisteredType> tryValidate(RegisteredType item, final RegisteredTypeLoadingContext constraint) {
// kept as a Maybe in case someone wants a wrapper around item validity;
// unclear what the contract should be, as this can return Maybe.Present(null)
// which is suprising, but it is more natural to callers otherwise they'll likely do a separate null check on the item
// (often handling null different to errors) so the Maybe.get() is redundant as they have an object for the input anyway.
if (item==null || constraint==null) return Maybe.ofDisallowingNull(item);
if (constraint.getExpectedKind()!=null && !constraint.getExpectedKind().equals(item.getKind()))
return Maybe.absent(item+" is not the expected kind "+constraint.getExpectedKind());
if (constraint.getExpectedJavaSuperType()!=null) {
if (!isSubtypeOf(item, constraint.getExpectedJavaSuperType())) {
return Maybe.absent(item+" is not for the expected type "+constraint.getExpectedJavaSuperType());
}
}
return Maybe.of(item);
}
/**
* Checks whether the given object appears to be an instance of the given registered type */
private static boolean isSubtypeOf(Class<?> candidate, RegisteredType type) {
for (Object st: type.getSuperTypes()) {
if (st instanceof RegisteredType) {
if (!isSubtypeOf(candidate, (RegisteredType)st)) return false;
}
if (st instanceof Class) {
if (!((Class<?>)st).isAssignableFrom(candidate)) return false;
}
}
return true;
}
public static RegisteredType getBestVersion(Iterable<RegisteredType> types) {
if (types==null || !types.iterator().hasNext()) return null;
return Ordering.from(RegisteredTypeComparator.INSTANCE).max(types);
}
public static class RegisteredTypeComparator implements Comparator<RegisteredType> {
public static Comparator<RegisteredType> INSTANCE = new RegisteredTypeComparator();
private RegisteredTypeComparator() {}
@Override
public int compare(RegisteredType o1, RegisteredType o2) {
return ComparisonChain.start()
.compareTrueFirst(o1.isDisabled(), o2.isDisabled())
.compareTrueFirst(o1.isDeprecated(), o2.isDeprecated())
.compare(o1.getSymbolicName(), o2.getSymbolicName(), NaturalOrderComparator.INSTANCE)
.compare(o1.getVersion(), o2.getVersion(), VersionComparator.INSTANCE)
.result();
}
}
/** validates that the given object (required) satisfies the constraints implied by the given
* type and context object, using {@link Maybe} as the result set absent containing the error(s)
* if not satisfied. returns an {@link Absent} if failed with details of the error,
* with {@link Absent#isNull()} true if the object is null. */
public static <T> Maybe<T> tryValidate(final T object, @Nullable final RegisteredType type, @Nullable final RegisteredTypeLoadingContext context) {
if (object==null) return Maybe.absentNull("object is null");
RegisteredTypeKind kind = type!=null ? type.getKind() : context!=null ? context.getExpectedKind() : null;
if (kind==null) {
if (object instanceof AbstractBrooklynObjectSpec) kind=RegisteredTypeKind.SPEC;
else kind=RegisteredTypeKind.BEAN;
}
return new RegisteredTypeKindVisitor<Maybe<T>>() {
@Override
protected Maybe<T> visitSpec() {
return tryValidateSpec(object, type, context);
}
@Override
protected Maybe<T> visitBean() {
return tryValidateBean(object, type, context);
}
}.visit(kind);
}
private static <T> Maybe<T> tryValidateBean(T object, RegisteredType type, final RegisteredTypeLoadingContext context) {
if (object==null) return Maybe.absentNull("object is null");
if (type!=null) {
if (type.getKind()!=RegisteredTypeKind.BEAN)
return Maybe.absent("Validating a bean when type is "+type.getKind()+" "+type);
if (!isSubtypeOf(object.getClass(), type))
return Maybe.absent(object+" does not have all the java supertypes of "+type);
}
if (context!=null) {
if (context.getExpectedKind()!=RegisteredTypeKind.BEAN)
return Maybe.absent("Validating a bean when constraint expected "+context.getExpectedKind());
if (context.getExpectedJavaSuperType()!=null && !context.getExpectedJavaSuperType().isInstance(object))
return Maybe.absent(object+" is not of the expected java supertype "+context.getExpectedJavaSuperType());
}
return Maybe.of(object);
}
private static <T> Maybe<T> tryValidateSpec(T object, RegisteredType rType, final RegisteredTypeLoadingContext constraint) {
if (object==null) return Maybe.absentNull("object is null");
if (!(object instanceof AbstractBrooklynObjectSpec)) {
Maybe.absent("Found "+object+" when expecting a spec");
}
Class<?> targetType = ((AbstractBrooklynObjectSpec<?,?>)object).getType();
if (targetType==null) {
Maybe.absent("Spec "+object+" does not have a target type");
}
if (rType!=null) {
if (rType.getKind()!=RegisteredTypeKind.SPEC)
Maybe.absent("Validating a spec when type is "+rType.getKind()+" "+rType);
if (!isSubtypeOf(targetType, rType))
Maybe.absent(object+" does not have all the java supertypes of "+rType);
}
if (constraint!=null) {
if (constraint.getExpectedJavaSuperType()!=null) {
if (!constraint.getExpectedJavaSuperType().isAssignableFrom(targetType)) {
Maybe.absent(object+" does not target the expected java supertype "+constraint.getExpectedJavaSuperType());
}
if (constraint.getExpectedJavaSuperType().isAssignableFrom(BrooklynObjectInternal.class)) {
// don't check spec type; any spec is acceptable
} else {
@SuppressWarnings("unchecked")
Class<? extends AbstractBrooklynObjectSpec<?, ?>> specType = RegisteredTypeLoadingContexts.lookupSpecTypeForTarget( (Class<? extends BrooklynObject>) constraint.getExpectedJavaSuperType());
if (specType==null) {
// means a problem in our classification of spec types!
Maybe.absent(object+" is returned as spec for unexpected java supertype "+constraint.getExpectedJavaSuperType());
}
if (!specType.isAssignableFrom(object.getClass())) {
Maybe.absent(object+" is not a spec of the expected java supertype "+constraint.getExpectedJavaSuperType());
}
}
}
}
return Maybe.of(object);
}
}