/*
* Copyright (c) 2015 EMC Corporation
* All Rights Reserved
*/
package com.emc.sa.asset.annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.PostConstruct;
import org.apache.log4j.Logger;
import com.emc.sa.asset.AbstractAssetOptionsProvider;
import com.emc.sa.asset.AssetOptionsContext;
import com.emc.sa.asset.AssetOptionsMethodInfo;
import com.emc.sa.asset.AssetOptionsProvider;
import com.emc.vipr.model.catalog.AssetOption;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
/**
* Abstract base class for all Asset Options Providers that use the {@link AssetNamespace} and {@link Asset} annotations.
*
* An AssetOptionsProvider should use the {@link AssetNamespace} and {@link Asset} annotations in the following way.
* (Methods must be public and return a {@link List}<{@link AssetOption}>)
*
* <pre>
* @AssetNamespace("myAssets")
* public class MyForDataProvider extends AssetOptionsProvider {
* @Asset("vcenter")
* public static List<AssetOptionsProvider.Option> getVCenters() {
* // ...
* }
*
* @Asset(value = "host", dependsOn = { "vcenter" })
* public static List<AssetOptionsProvider.Option> getHosts(String vcenter) {
* // ...
* }
* }
* </pre>
*
* The above provider registers two Assets, myAssets.vcenter and myAssets.host. When fetching a host, it also specifies
* that a vcenter assetType name should be passed.
*
* The parameter names are not important, but the order of the items in the 'dependsOn' list /IS/. This is how the
* provider figures out how to whether the 'required parameters' requirements have been met.
*
*/
public abstract class AnnotatedAssetOptionsProvider extends AbstractAssetOptionsProvider implements AssetOptionsProvider {
private static final Logger log = Logger.getLogger(AnnotatedAssetOptionsProvider.class);
/**
* a map of the supported asset types and the corresponding list of AssetOptionsMethods which are available to
* support that asset type.
*/
protected Map<String, List<AssetOptionsMethodInfo>> supportedAssetTypes = Maps.newHashMap();
@PostConstruct
public void init() {
// discover all the asset options provided by this implementation and store them for later
storeAssetOptionsMethods(discoverAssetsForClass());
}
@Override
public boolean isAssetTypeSupported(String assetTypeName) {
// if the supportedAssetTypes list has an entry for this asset type name then it's supported
return supportedAssetTypes.containsKey(assetTypeName);
}
/**
* Return all the possible options for the given assetType
*/
@Override
public List<AssetOption> getAssetOptions(AssetOptionsContext context, String assetTypeName,
Map<String, String> availableAssets) {
// find the asset options method for the given asset type name
final AssetOptionsMethodInfo assetMethod = findAssetMethodInfo(assetTypeName, availableAssets.keySet());
log.debug(String.format("Calling asset method [%s] with available assets [%s]", assetMethod.javaMethod.getName(), availableAssets));
try {
return invokeAssetMethod(assetMethod, context, availableAssets);
} catch (Exception e) {
log.error("Error invoking assetType retrieval method " + assetMethod.javaMethod.getName(), e);
if (e instanceof RuntimeException) {
throw (RuntimeException) e;
} else {
throw new RuntimeException("Error invoking assetType retrieval method " + assetMethod.javaMethod.getName(), e);
}
}
}
@Override
public List<String> getAssetDependencies(String assetType, Set<String> availableTypes) {
AssetOptionsMethodInfo method = findAssetMethodInfo(assetType, availableTypes);
if (method == null) {
StringBuffer buffer = new StringBuffer();
for (AssetOptionsMethodInfo assetMethod : supportedAssetTypes.get(assetType)) {
buffer.append("{" + assetMethod.assetNamespace + "." + assetMethod.assetName + "} " +
assetMethod.javaMethod.getName() + " " + assetMethod.assetDependencies + "\n");
}
throw new RuntimeException("Unable to find asset retrieval method for " + assetType + " with available types "
+ availableTypes.toString() + "\n" +
"Possible matches : \n" + buffer.toString());
}
return method.assetDependencies;
}
/**
* stores the asset options method information in the supportedAssetTypes list
*/
public void storeAssetOptionsMethods(List<AssetOptionsMethodInfo> assetMethods) {
// record the assets for later
for (AssetOptionsMethodInfo annotatedMethodInfo : assetMethods) {
final String assetTypeName = annotatedMethodInfo.assetName;
// if the supported asset types map doesn't already contain an entry
// for this asset type we need to add one
if (!supportedAssetTypes.containsKey(assetTypeName)) {
supportedAssetTypes.put(assetTypeName, Lists.<AssetOptionsMethodInfo> newArrayList());
}
// add the annotated method we found to the supported asset types map
// using the asset type name as the key
supportedAssetTypes.get(assetTypeName).add(annotatedMethodInfo);
}
}
/**
* Find the methods that have the @{@link Asset} annotation and have the correct signature
*/
public List<AssetOptionsMethodInfo> discoverAssetsForClass() {
final Class<? extends AssetOptionsProvider> providerClass = this.getClass();
final AssetNamespace namespace = (AssetNamespace) providerClass.getAnnotation(AssetNamespace.class);
final List<AssetOptionsMethodInfo> assetMethods = Lists.newArrayList();
if (namespace != null) {
for (Method javaMethod : providerClass.getMethods()) {
if (isAssetMethod(javaMethod)) {
// Ensures that the asset method parameters/dependencies match
validateMethodSignature(javaMethod);
final Asset asset = javaMethod.getAnnotation(Asset.class);
assetMethods.add(new AssetOptionsMethodInfo(namespace, asset, javaMethod));
}
}
}
return assetMethods;
}
/**
* Return true if the given java method is an 'Asset' method.
* Which is to say, if the method is annotated with the @{@link Asset} annotation and
* has the correct signature: <code>public List<AssetOption> (AssetOptionsContext context, ...)</code>
*/
private boolean isAssetMethod(Method javaMethod) {
final boolean isAnnotated = javaMethod.getAnnotation(Asset.class) != null;
final boolean isPublic = Modifier.isPublic(javaMethod.getModifiers());
final boolean returnsAssetOptionsList = List.class.isAssignableFrom(javaMethod.getReturnType());
final boolean firstParamIsContext = isFirstParamAssetOptionsContext(javaMethod);
final boolean correctSignature = isPublic && returnsAssetOptionsList && firstParamIsContext;
// if the method is annotated with @Asset but doesn't have the correct signature we need to return
// false, but we can also put a message in the log about the problem
if (isAnnotated && !correctSignature) {
log.error(String
.format("Method %s::%s has the @Asset annotation but does not conform to the correct signature: public List<%s> (AssetOptionsContext context, ...)",
javaMethod.getDeclaringClass().getName(), javaMethod.getName(), AssetOption.class.getSimpleName()));
return false;
}
return isAnnotated && correctSignature;
}
/**
* return true if the given java method has at least one parameter
* and the first one is an {@link AssetOptionsContext}
*/
private boolean isFirstParamAssetOptionsContext(Method javaMethod) {
final boolean hasParams = javaMethod.getParameterTypes().length > 0;
if (hasParams) {
final Class<?> firstParamType = javaMethod.getParameterTypes()[0];
return firstParamType.isAssignableFrom(AssetOptionsContext.class);
}
return false;
}
/**
* Invoke the given {@link AssetOptionsMethodInfo} using the available asset information
*/
@SuppressWarnings("unchecked")
public List<AssetOption> invokeAssetMethod(AssetOptionsMethodInfo assetMethod, AssetOptionsContext context,
Map<String, String> availableAssets)
throws IllegalAccessException, InvocationTargetException {
final List<Object> javaMethodParameters = buildJavaMethodParameters(context, availableAssets, assetMethod);
try {
return (List<AssetOption>) assetMethod.javaMethod.invoke(this, javaMethodParameters.toArray());
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
} else {
throw e;
}
}
}
/**
* Build the list of parameter objects to hand in to the method invocation
*/
public List<Object> buildJavaMethodParameters(AssetOptionsContext context, Map<String, String> availableAssets,
AssetOptionsMethodInfo assetMethod) {
final List<Object> javaMethodParameters = Lists.newArrayList();
// add the context
javaMethodParameters.add(context);
// cycle through the asset dependencies and lookup the value
// for that dependency in the available assets map and add it
// to the javaMethodParameters list for use in the method invocation.
for (String parentAssetName : assetMethod.assetDependencies) {
String parentAssetValue = availableAssets.get(parentAssetName);
int index = javaMethodParameters.size();
Object value = assetMethod.convertParameter(index, parentAssetValue);
javaMethodParameters.add(value);
}
return javaMethodParameters;
}
/**
* Finds a possible method that can be called for the given assetType type and the given parameter names
*
* @param assetRetrialMethods
*/
public AssetOptionsMethodInfo findAssetMethodInfo(String assetTypeName, Set<String> availableAssets) {
if (!supportedAssetTypes.containsKey(assetTypeName)) {
throw new RuntimeException(String.format("No asset found with name: %s", assetTypeName));
}
int lastMatch = -1;
AssetOptionsMethodInfo foundMatch = null;
for (AssetOptionsMethodInfo info : supportedAssetTypes.get(assetTypeName)) {
int matchingParentAssets = 0;
for (String parentAsset : availableAssets) {
if (info.assetDependencies.contains(parentAsset)) {
matchingParentAssets++;
}
}
// Only use this method if it has MORE matching parameters than the previous one found
if (info.assetDependencies.size() == matchingParentAssets) {
if (info.assetDependencies.size() > lastMatch) {
foundMatch = info;
lastMatch = info.assetDependencies.size();
}
}
}
if (foundMatch == null) {
final String errorMessage = String.format("Query for Asset '%s' requires additional asset dependencies. Supplied: %s",
assetTypeName, availableAssets.toString());
log.warn(errorMessage);
throw new IllegalStateException(errorMessage);
}
return foundMatch;
}
/**
* Dumps all registered assets along with the registered methods
*/
@SuppressWarnings("unused")
private void dump() {
for (Map.Entry<String, List<AssetOptionsMethodInfo>> entry : supportedAssetTypes.entrySet()) {
System.out.println("*" + entry.getKey());
for (AssetOptionsMethodInfo info : entry.getValue()) {
System.out.println("--" + info.javaMethod.getName());
System.out.println("-- Requires:");
for (String parentAsset : info.assetDependencies) {
System.out.println("-----" + parentAsset);
}
}
}
}
/**
* Validates that the provided method matches the expected signature.
*
* @param method
* the method to validate.
*/
public static void validateMethodSignature(Method method) {
Class<?>[] parameterTypes = method.getParameterTypes();
// Ensure the first parameter is an asset options context
if ((parameterTypes.length == 0) || !parameterTypes[0].isAssignableFrom(AssetOptionsContext.class)) {
throw new IllegalArgumentException("AssetOptionsContext must be the first parameter: "
+ method.toGenericString());
}
// Ensure the method arguments match the number of dependencies
int requiredNumberOfDependencies = parameterTypes.length - 1;
AssetDependencies dependencies = method.getAnnotation(AssetDependencies.class);
int dependencyCount = (dependencies != null) ? dependencies.value().length : 0;
if (dependencyCount < requiredNumberOfDependencies) {
throw new IllegalArgumentException("Method does not have enough parameters to satisfy dependencies: "
+ method.toGenericString());
}
else if (dependencyCount > requiredNumberOfDependencies) {
throw new IllegalArgumentException("Method has more parameters than will be provided by dependencies: "
+ method.toGenericString());
}
}
}