/*
* 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.entity.drivers;
import java.lang.reflect.Constructor;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.apache.brooklyn.api.entity.drivers.DriverDependentEntity;
import org.apache.brooklyn.api.entity.drivers.EntityDriver;
import org.apache.brooklyn.api.location.Location;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.brooklyn.location.paas.PaasLocation;
import org.apache.brooklyn.location.ssh.SshMachineLocation;
import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.exceptions.ReferenceWithError;
import org.apache.brooklyn.util.text.Strings;
/**
* Follows a class naming convention: the driver interface typically ends in "Driver", and the implementation
* must match the driver interface name but with a suffix like "SshDriver" instead of "Driver".
* Other rules can be added using {@link #addRule(String, DriverInferenceRule)} or
* {@link #addClassFullNameMapping(String, String)}.
* <p>
* Reflectively instantiates and returns the driver, based on the location passed in,
* in {@link #build(DriverDependentEntity, Location)}.
*
* @author Peter Veentjer, Alex Heneveld
*/
public class ReflectiveEntityDriverFactory {
private static final Logger LOG = LoggerFactory.getLogger(ReflectiveEntityDriverFactory.class);
/** Rules, keyed by a unique identifier. Executed in order of most-recently added first. */
protected final Map<String,DriverInferenceRule> rules = MutableMap.of();
public ReflectiveEntityDriverFactory() {
addRule(DriverInferenceForSshLocation.DEFAULT_IDENTIFIER, new DriverInferenceForSshLocation());
addRule(DriverInferenceForPaasLocation.DEFAULT_IDENTIFIER, new DriverInferenceForPaasLocation());
addRule(DriverInferenceForWinRmLocation.DEFAULT_IDENTIFIER, new DriverInferenceForWinRmLocation());
}
public interface DriverInferenceRule {
public <D extends EntityDriver> ReferenceWithError<Class<? extends D>> resolve(DriverDependentEntity<D> entity, Class<D> driverInterface, Location location);
}
public static abstract class AbstractDriverInferenceRule implements DriverInferenceRule {
@Override
public <D extends EntityDriver> ReferenceWithError<Class<? extends D>> resolve(DriverDependentEntity<D> entity, Class<D> driverInterface, Location location) {
try {
String newName = inferDriverClassName(entity, driverInterface, location);
if (newName==null) return null;
return loadDriverClass(newName, entity, driverInterface);
} catch (Exception e) {
Exceptions.propagateIfFatal(e);
return ReferenceWithError.newInstanceThrowingError(null, e);
}
}
public abstract <D extends EntityDriver> String inferDriverClassName(DriverDependentEntity<D> entity, Class<D> driverInterface, Location location);
protected <D extends EntityDriver> ReferenceWithError<Class<? extends D>> loadDriverClass(String className, DriverDependentEntity<D> entity, Class<D> driverInterface) {
ReferenceWithError<Class<? extends D>> r1 = loadClass(className, entity.getClass().getClassLoader());
if (!r1.hasError()) return r1;
ReferenceWithError<Class<? extends D>> r2 = loadClass(className, driverInterface.getClass().getClassLoader());
if (!r2.hasError()) return r2;
return r1;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
protected <D extends EntityDriver> ReferenceWithError<Class<? extends D>> loadClass(String className, ClassLoader classLoader) {
try {
return (ReferenceWithError<Class<? extends D>>)(ReferenceWithError) ReferenceWithError.newInstanceWithoutError((Class<? extends EntityDriver>)classLoader.loadClass(className));
} catch (Exception e) {
Exceptions.propagateIfFatal(e);
return ReferenceWithError.newInstanceThrowingError(null, e);
}
}
}
public static abstract class AbstractDriverInferenceRenamingInferenceRule extends AbstractDriverInferenceRule {
protected final String expectedPattern;
protected final String replacement;
public AbstractDriverInferenceRenamingInferenceRule(String expectedPattern, String replacement) {
this.expectedPattern = expectedPattern;
this.replacement = replacement;
}
public String getIdentifier() {
return getClass().getName()+"["+expectedPattern+"]";
}
@Override
public String toString() {
return getClass().getName()+"["+expectedPattern+"->"+replacement+"]";
}
}
public static class DriverInferenceByRenamingClassFullName extends AbstractDriverInferenceRenamingInferenceRule {
public DriverInferenceByRenamingClassFullName(String expectedClassFullName, String newClassFullName) {
super(expectedClassFullName, newClassFullName);
}
@Override
public <D extends EntityDriver> String inferDriverClassName(DriverDependentEntity<D> entity, Class<D> driverInterface, Location location) {
if (driverInterface.getName().equals(expectedPattern)) {
return replacement;
}
return null;
}
}
public static class DriverInferenceByRenamingClassSimpleName extends AbstractDriverInferenceRenamingInferenceRule {
public DriverInferenceByRenamingClassSimpleName(String expectedClassSimpleName, String newClassSimpleName) {
super(expectedClassSimpleName, newClassSimpleName);
}
@Override
public <D extends EntityDriver> String inferDriverClassName(DriverDependentEntity<D> entity, Class<D> driverInterface, Location location) {
if (driverInterface.getSimpleName().equals(expectedPattern)) {
// i'd like to do away with drivers altogether, but if people *really* need to use this and suppress the warning,
// they can use the full class rename
LOG.warn("Using discouraged driver simple class rename to find "+replacement+" for "+expectedPattern+"; it is recommended to set getDriverInterface() or newDriver() appropriately");
return Strings.removeFromEnd(driverInterface.getName(), expectedPattern)+replacement;
}
return null;
}
}
public static class DriverInferenceForSshLocation extends AbstractDriverInferenceRule {
public static final String DEFAULT_IDENTIFIER = "ssh-location-driver-inference-rule";
@Override
public <D extends EntityDriver> String inferDriverClassName(DriverDependentEntity<D> entity, Class<D> driverInterface, Location location) {
String driverInterfaceName = driverInterface.getName();
if (!(location instanceof SshMachineLocation)) return null;
if (!driverInterfaceName.endsWith("Driver")) {
throw new IllegalArgumentException(String.format("Driver name [%s] doesn't end with 'Driver'; cannot auto-detect SshDriver class name", driverInterfaceName));
}
return Strings.removeFromEnd(driverInterfaceName, "Driver")+"SshDriver";
}
}
public static class DriverInferenceForPaasLocation extends AbstractDriverInferenceRule {
public static final String DEFAULT_IDENTIFIER = "paas-location-driver-inference-rule";
@Override
public <D extends EntityDriver> String inferDriverClassName(DriverDependentEntity<D> entity, Class<D> driverInterface, Location location) {
String driverInterfaceName = driverInterface.getName();
if (!(location instanceof PaasLocation)) return null;
if (!driverInterfaceName.endsWith("Driver")) {
throw new IllegalArgumentException(String.format("Driver name [%s] doesn't end with 'Driver'; cannot auto-detect PaasDriver class name", driverInterfaceName));
}
return Strings.removeFromEnd(driverInterfaceName, "Driver") + ((PaasLocation) location).getPaasProviderName() + "Driver";
}
}
public static class DriverInferenceForWinRmLocation extends AbstractDriverInferenceRule {
public static final String DEFAULT_IDENTIFIER = "winrm-location-driver-inference-rule";
@Override
public <D extends EntityDriver> String inferDriverClassName(DriverDependentEntity<D> entity, Class<D> driverInterface, Location location) {
String driverInterfaceName = driverInterface.getName();
// TODO: use a proper registry later on
try {
if (!Class.forName("org.apache.brooklyn.location.winrm.WinRmMachineLocation").isInstance(location)) return null;
} catch (ClassNotFoundException ex) {
return null;
}
if (!driverInterfaceName.endsWith("Driver")) {
throw new IllegalArgumentException(String.format("Driver name [%s] doesn't end with 'Driver'; cannot auto-detect WinRmDriver class name", driverInterfaceName));
}
return Strings.removeFromEnd(driverInterfaceName, "Driver")+"WinRmDriver";
}
}
/** adds a rule; possibly replacing an old one if one exists with the given identifier. the new rule is added after all previous ones.
* @return the replaced rule, or null if there was no old rule */
public DriverInferenceRule addRule(String identifier, DriverInferenceRule rule) {
DriverInferenceRule oldRule = rules.remove(identifier);
rules.put(identifier, rule);
LOG.debug("Added driver mapping rule "+rule);
return oldRule;
}
public DriverInferenceRule addClassFullNameMapping(String expectedClassFullName, String newClassFullName) {
DriverInferenceByRenamingClassFullName rule = new DriverInferenceByRenamingClassFullName(expectedClassFullName, newClassFullName);
return addRule(rule.getIdentifier(), rule);
}
public DriverInferenceRule addClassSimpleNameMapping(String expectedClassSimpleName, String newClassSimpleName) {
DriverInferenceByRenamingClassSimpleName rule = new DriverInferenceByRenamingClassSimpleName(expectedClassSimpleName, newClassSimpleName);
return addRule(rule.getIdentifier(), rule);
}
public <D extends EntityDriver> D build(DriverDependentEntity<D> entity, Location location){
Class<D> driverInterface = entity.getDriverInterface();
Class<? extends D> driverClass = null;
List<Throwable> exceptions = MutableList.of();
if (driverInterface.isInterface()) {
List<DriverInferenceRule> ruleListInExecutionOrder = MutableList.copyOf(rules.values());
Collections.reverse(ruleListInExecutionOrder);
// above puts rules in order with most recently added first
for (DriverInferenceRule rule: ruleListInExecutionOrder) {
ReferenceWithError<Class<? extends D>> clazzR = rule.resolve(entity, driverInterface, location);
if (clazzR!=null) {
if (!clazzR.hasError()) {
Class<? extends D> clazz = clazzR.get();
if (clazz!=null) {
driverClass = clazz;
break;
}
} else {
exceptions.add(clazzR.getError());
}
}
}
} else {
driverClass = driverInterface;
}
LOG.debug("Driver for "+driverInterface.getName()+" in "+location+" is: "+driverClass);
if (driverClass==null) {
if (exceptions.isEmpty())
throw new RuntimeException("No drivers could be found for "+driverInterface.getName()+"; "
+ "currently only SshMachineLocation is supported for autodetection (location "+location+")");
else throw Exceptions.create("No drivers could be loaded for "+driverInterface.getName()+" in "+location, exceptions);
}
try {
Constructor<? extends D> constructor = getConstructor(driverClass);
constructor.setAccessible(true);
return constructor.newInstance(entity, location);
} catch (Exception e) {
LOG.warn("Unable to instantiate "+driverClass+" (rethrowing): "+e);
throw Exceptions.propagate(e);
}
}
@SuppressWarnings("unchecked")
private <D extends EntityDriver> Constructor<D> getConstructor(Class<D> driverClass) {
for (Constructor<?> constructor : driverClass.getConstructors()) {
if (constructor.getParameterTypes().length == 2) {
return (Constructor<D>) constructor;
}
}
throw new RuntimeException(String.format("Class [%s] has no constructor with 2 arguments", driverClass.getName()));
}
}