/*
* Copyright (c) 2013 EMC Corporation
* All Rights Reserved
*/
package com.emc.storageos.svcs.errorhandling.model;
import static java.lang.Thread.currentThread;
import static java.lang.reflect.Proxy.newProxyInstance;
import static org.apache.commons.lang.StringUtils.isBlank;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.concurrent.ConcurrentHashMap;
import com.emc.storageos.svcs.errorhandling.annotations.DeclareServiceCode;
import com.emc.storageos.svcs.errorhandling.annotations.MessageBundle;
import com.emc.storageos.svcs.errorhandling.resources.ServiceCode;
import com.emc.storageos.svcs.errorhandling.utils.MessageUtils;
/**
* This is the class that does the actual work of generating the Exceptions and messages.
* You should only ever create an instance of this class by using {@link #create(Class)} and passing in the interface you want proxy.
* The specified Interface must be annotated with {@link MessageBundle} and all the methods must be annotated with
* {@link DeclareServiceCode}.
* <p/>
* For more information or to see an example, check the Developers Guide section in the Error Handling Wiki page:
* http://confluence.lab.voyence.com/display/OS/Error+Handling+Framework+and+Exceptions+in+ViPR
*
* @author fountt1
*/
public final class ExceptionMessagesProxy implements InvocationHandler {
private static final ConcurrentHashMap<Class<?>, Object> proxyMap = new ConcurrentHashMap<Class<?>, Object>();
@SuppressWarnings("unchecked")
public static synchronized <T> T create(final Class<T> interfaze) {
if (!proxyMap.containsKey(interfaze)) {
final ClassLoader loader = currentThread().getContextClassLoader();
final Class<?>[] interfaces = new Class<?>[] { interfaze };
final InvocationHandler handler = new ExceptionMessagesProxy();
final T instance = (T) newProxyInstance(loader, interfaces, handler);
proxyMap.put(interfaze, instance);
}
return (T) proxyMap.get(interfaze);
}
@Override
public Object invoke(final Object proxy, final Method method, final Object[] args)
throws Throwable {
final Class<?> type = type(method);
final String detailBase = detailBase(method);
final String detailKey = method.getName();
final ServiceCode serviceCode = serviceCode(method);
final Throwable cause = cause(args);
if (ServiceError.class.isAssignableFrom(type)) {
return contructServiceError(serviceCode, detailBase, detailKey, convertThrowableMessages(args));
}
Exception exception = contructException(type, serviceCode, cause, detailBase,
detailKey, convertThrowableMessages(args));
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
// removing the following 3 stack trace elements from the trackstrace:
// 0: Thread#getStackTrace
// 1: ExceptionMessagesProxy#invoke
// 2: com.sun.proxy.$ProxyXX#<methodName>
exception.setStackTrace(Arrays.copyOfRange(stackTrace, 3, stackTrace.length));
return exception;
}
/**
* Converts all Throwable arguments to its message if the message is not null
*
* @param args array of arguments
* @return array of arguments
*/
private static Object[] convertThrowableMessages(final Object[] args) {
if (args != null) {
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof Throwable) {
Throwable t = (Throwable) args[i];
if (t.getMessage() != null) {
args[i] = t.getMessage();
}
}
}
}
return args;
}
/**
* Get the return type of the method and check if it is a valid type
*
* @param method
* @return
*/
private Class<?> type(final Method method) {
final Class<?> type = method.getReturnType();
if (Modifier.isAbstract(type.getModifiers())) {
throw new IllegalStateException("Cannot create instances of an abstract class");
}
if (!ServiceCoded.class.isAssignableFrom(type)
|| !(Exception.class.isAssignableFrom(type) || ServiceError.class
.isAssignableFrom(type))) {
throw new IllegalStateException(
"Return type must be of type ServiceCoded and may also be an Exception");
}
return type;
}
/**
* Get the specified {@link ServiceCode} from the {@link DeclareServiceCode} annotation
*
* @param method
* @return
*/
private ServiceCode serviceCode(final Method method) {
final DeclareServiceCode declaredServiceCode = method
.getAnnotation(DeclareServiceCode.class);
if (declaredServiceCode == null) {
throw new IllegalStateException(
"A service code must be provided via @DeclareServiceCode");
}
final ServiceCode serviceCode = declaredServiceCode.value();
return serviceCode;
}
/**
* Get the name of the message bundle to use when getting the message for the specified method
*
* @param method
* @return
*/
private String detailBase(final Method method) {
final Class<?> clazz = method.getDeclaringClass();
final String detailBase = MessageUtils.bundleNameForClass(clazz);
if (isBlank(detailBase)) {
throw new IllegalStateException("no bundle name defined for " + clazz);
}
return detailBase;
}
/**
* Find the cause argument if it has been past into the method
*
* @param args
* @return
*/
private Throwable cause(final Object[] args) {
Throwable t = null;
if (args != null) {
for (final Object arg : args) {
if (arg instanceof Throwable) {
t = (Throwable) arg;
break;
}
}
}
return t;
}
/**
* Construct the Exception being returned by the method, using a known standard constructor
*
* @param type
* @param serviceCode
* @param cause
* @param detailBase
* @param detailKey
* @param args
* @return
* @throws NoSuchMethodException
* @throws InstantiationException
* @throws IllegalAccessException
* @throws InvocationTargetException
*/
private Exception contructException(final Class<?> type, final ServiceCode serviceCode,
final Throwable cause, final String detailBase, final String detailKey,
final Object[] args) throws NoSuchMethodException, InstantiationException,
IllegalAccessException, InvocationTargetException {
final Constructor<?> constructor = type.getDeclaredConstructor(ServiceCode.class,
Throwable.class, String.class, String.class, Object[].class);
constructor.setAccessible(true);
return (Exception) constructor.newInstance(serviceCode, cause, detailBase, detailKey, args);
}
private ServiceError contructServiceError(final ServiceCode serviceCode,
final String detailBase, final String detailKey, final Object[] args)
throws NoSuchMethodException, InstantiationException, IllegalAccessException,
InvocationTargetException {
final Constructor<ServiceError> constructor = ServiceError.class.getDeclaredConstructor(
ServiceCode.class, String.class, String.class, Object[].class);
constructor.setAccessible(true);
return constructor.newInstance(serviceCode, detailBase, detailKey, args);
}
}