blob: d63ff7ad9fe953f5a6011fb9ef71ac823a82ad25 [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.camp.brooklyn.spi.creation;
import java.util.Map;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.apache.brooklyn.api.mgmt.classloading.BrooklynClassLoadingContext;
import org.apache.brooklyn.api.objs.Configurable;
import org.apache.brooklyn.camp.brooklyn.BrooklynCampReservedKeys;
import org.apache.brooklyn.core.objs.BrooklynObjectInternal.ConfigurationSupportInternal;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.core.config.ConfigBag;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.guava.Maybe;
import org.apache.brooklyn.util.javalang.Reflections;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.annotations.Beta;
import com.google.common.base.Preconditions;
/** Assists in loading types referenced from YAML;
* mainly as a way to share logic used in very different contexts. */
public abstract class BrooklynYamlTypeInstantiator {
private static final Logger log = LoggerFactory.getLogger(BrooklynYamlTypeInstantiator.class);
protected final Factory factory;
@Beta
public static class Factory {
final BrooklynClassLoadingContext loader;
final Object contextForLogging;
public Factory(BrooklynClassLoadingContext loader, Object contextForLogging) {
this.loader = loader;
this.contextForLogging = contextForLogging;
}
public InstantiatorFromKey from(Map<?,?> data) {
return new InstantiatorFromKey(this, ConfigBag.newInstance(data));
}
public InstantiatorFromKey from(ConfigBag data) {
return new InstantiatorFromKey(this, data);
}
public InstantiatorFromName type(String typeName) {
return new InstantiatorFromName(this, typeName);
}
}
public static class InstantiatorFromKey extends BrooklynYamlTypeInstantiator {
protected final ConfigBag data;
protected String typeKeyPrefix = null;
/** Nullable only permitted for instances which do not do loading, e.g. LoaderFromKey#lookup */
protected InstantiatorFromKey(@Nullable Factory factory, ConfigBag data) {
super(factory);
this.data = data;
}
public static Maybe<String> extractTypeName(String prefix, ConfigBag data) {
if (data==null) return Maybe.absent();
return new InstantiatorFromKey(null, data).prefix(prefix).getTypeName();
}
public InstantiatorFromKey prefix(String prefix) {
typeKeyPrefix = prefix;
return this;
}
@Override
public Maybe<String> getTypeName() {
Maybe<Object> result = data.getStringKeyMaybe(getTypedKeyName());
if (result.isAbsent() && typeKeyPrefix!=null) {
// try alternatives if a prefix was specified
result = data.getStringKeyMaybe(typeKeyPrefix+"Type");
if (result.isAbsent()) result = data.getStringKeyMaybe("type");
}
if (result.isAbsent() || result.get()==null)
return Maybe.absent("Missing key 'type'"+(typeKeyPrefix!=null ? " (or '"+getTypedKeyName()+"')" : ""));
if (result.get() instanceof String) return Maybe.of((String)result.get());
throw new IllegalArgumentException("Invalid value "+result.get().getClass()+" for "+getTypedKeyName()+"; "
+ "expected String, got "+result.get());
}
protected String getTypedKeyName() {
if (typeKeyPrefix!=null) return typeKeyPrefix+"_type";
return "type";
}
/** as {@link #newInstance(Class)} but inferring the type */
public Object newInstance() {
return newInstance(null);
}
/** creates a new instance of the type referred to by this description,
* as a subtype of the type supplied here,
* inferring a Map from <code>brooklyn.config</code> key.
* TODO in future also picking up recognized flags and config keys (those declared on the type).
* <p>
* constructs the object using:
* <li> a constructor on the class taking a Map
* <li> a no-arg constructor, only if the inferred map is empty
**/
public <T> T newInstance(@Nullable Class<T> supertype) {
Class<? extends T> type = getType(supertype);
Map<String, ?> cfg = getConfigMap();
Maybe<? extends T> result = Reflections.invokeConstructorFromArgs(type, cfg);
if (result.isPresent())
return result.get();
ConfigBag cfgBag = ConfigBag.newInstance(cfg);
result = Reflections.invokeConstructorFromArgs(type, cfgBag);
if (result.isPresent())
return result.get();
if (cfg.isEmpty()) {
result = Reflections.invokeConstructorFromArgs(type);
if (result.isPresent())
return result.get();
} else if (Configurable.class.isAssignableFrom(type)) {
result = Reflections.invokeConstructorFromArgs(type);
if (result.isPresent()) {
ConfigurationSupportInternal configSupport = (ConfigurationSupportInternal) ((Configurable)result.get()).config();
configSupport.putAll(cfg);
return result.get();
}
}
throw new IllegalStateException("No known mechanism for constructing type "+type+" in "+factory.contextForLogging);
}
/** finds the map of config for the type specified;
* currently only gets <code>brooklyn.config</code>, returning empty map if none,
* but TODO in future should support recognized flags and config keys (those declared on the type),
* incorporating code in {@link BrooklynEntityMatcher}.
*/
@SuppressWarnings("unchecked")
@Nonnull
public Map<String,?> getConfigMap() {
MutableMap<String,Object> result = MutableMap.of();
Object bc = data.getStringKey(BrooklynCampReservedKeys.BROOKLYN_CONFIG);
if (bc!=null) {
if (bc instanceof Map)
result.putAll((Map<? extends String, ?>) bc);
else
throw new IllegalArgumentException("brooklyn.config key in "+factory.contextForLogging+" should be a map, not "+bc.getClass()+" ("+bc+")");
}
return result;
}
}
public static class InstantiatorFromName extends BrooklynYamlTypeInstantiator {
protected final String typeName;
protected InstantiatorFromName(Factory factory, String typeName) {
super(factory);
this.typeName = typeName;
}
@Override
public Maybe<String> getTypeName() {
return Maybe.fromNullable(typeName);
}
}
protected BrooklynYamlTypeInstantiator(Factory factory) {
this.factory = factory;
}
public abstract Maybe<String> getTypeName();
public BrooklynClassLoadingContext getClassLoadingContext() {
Preconditions.checkNotNull(factory, "No factory set; cannot use this instance for type loading");
return factory.loader;
}
public Class<?> getType() {
return getType(Object.class);
}
public <T> Class<? extends T> getType(@Nonnull Class<T> type) {
try {
return getClassLoadingContext().loadClass(getTypeName().get(), type);
} catch (Exception e) {
Exceptions.propagateIfFatal(e);
log.debug("Unable to resolve " + type + " " + getTypeName().get() + " (rethrowing) in spec " + factory.contextForLogging);
throw Exceptions.propagate(e);
}
}
}