/* *************************************************************************************** * Copyright (C) 2006 EsperTech, Inc. All rights reserved. * * http://www.espertech.com/esper * * http://www.espertech.com * * ---------------------------------------------------------------------------------- * * The software in this package is published under the terms of the GPL license * * a copy of which has been included with this distribution in the license.txt file. * *************************************************************************************** */ package com.espertech.esper.epl.annotation; import com.espertech.esper.client.EPStatementException; import com.espertech.esper.client.annotation.Hint; import com.espertech.esper.client.annotation.HintEnum; import com.espertech.esper.collection.Pair; import com.espertech.esper.epl.core.EngineImportException; import com.espertech.esper.epl.core.EngineImportService; import com.espertech.esper.epl.expression.core.ExprValidationException; import com.espertech.esper.epl.spec.AnnotationDesc; import com.espertech.esper.util.CollectionUtil; import com.espertech.esper.util.JavaClassHelper; import com.espertech.esper.util.SimpleTypeCaster; import com.espertech.esper.util.SimpleTypeCasterFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.annotation.Annotation; import java.lang.reflect.Array; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.*; /** * Utility to handle EPL statement annotations. */ public class AnnotationUtil { private static final Logger log = LoggerFactory.getLogger(AnnotationUtil.class); public static Map<String, List<AnnotationDesc>> mapByNameLowerCase(List<AnnotationDesc> annotations) { Map<String, List<AnnotationDesc>> map = new HashMap<String, List<AnnotationDesc>>(); for (AnnotationDesc desc : annotations) { String key = desc.getName().toLowerCase(Locale.ENGLISH); if (map.containsKey(key)) { map.get(key).add(desc); continue; } List<AnnotationDesc> annos = new ArrayList<AnnotationDesc>(2); annos.add(desc); map.put(key, annos); } return map; } public static Object getValue(AnnotationDesc desc) { for (Pair<String, Object> pair : desc.getAttributes()) { if (pair.getFirst().toLowerCase(Locale.ENGLISH).equals("value")) { return pair.getSecond(); } } return null; } /** * Compile annotation objects from descriptors. * * @param annotationSpec spec for annotations * @param engineImportService engine imports * @param eplStatement statement expression * @return annotations */ public static Annotation[] compileAnnotations(List<AnnotationDesc> annotationSpec, EngineImportService engineImportService, String eplStatement) { Annotation[] annotations; try { annotations = AnnotationUtil.compileAnnotations(annotationSpec, engineImportService); } catch (AnnotationException e) { throw new EPStatementException("Failed to process statement annotations: " + e.getMessage(), e, eplStatement); } catch (RuntimeException ex) { String message = "Unexpected exception compiling annotations in statement, please consult the log file and report the exception: " + ex.getMessage(); log.error(message, ex); throw new EPStatementException(message, ex, eplStatement); } return annotations; } /** * Compiles annotations to an annotation array. * * @param desc a list of descriptors * @param engineImportService for resolving the annotation class * @return annotations or empty array if none * @throws AnnotationException if annotations could not be created */ private static Annotation[] compileAnnotations(List<AnnotationDesc> desc, EngineImportService engineImportService) throws AnnotationException { Annotation[] annotations = new Annotation[desc.size()]; for (int i = 0; i < desc.size(); i++) { annotations[i] = createProxy(desc.get(i), engineImportService); if (annotations[i] instanceof Hint) { HintEnum.validateGetListed(annotations[i]); } } return annotations; } private static Annotation createProxy(AnnotationDesc desc, EngineImportService engineImportService) throws AnnotationException { // resolve class final Class annotationClass; try { annotationClass = engineImportService.resolveAnnotation(desc.getName()); } catch (EngineImportException e) { throw new AnnotationException("Failed to resolve @-annotation class: " + e.getMessage()); } // obtain Annotation class properties List<AnnotationAttribute> annotationAttributeLists = getAttributes(annotationClass); Set<String> allAttributes = new HashSet<String>(); Set<String> requiredAttributes = new LinkedHashSet<String>(); for (AnnotationAttribute annotationAttribute : annotationAttributeLists) { allAttributes.add(annotationAttribute.getName()); if (annotationAttribute.getDefaultValue() != null) { requiredAttributes.add(annotationAttribute.getName()); } } // get attribute values List<String> providedValues = new ArrayList<String>(); for (Pair<String, Object> annotationValuePair : desc.getAttributes()) { providedValues.add(annotationValuePair.getFirst()); } // for all attributes determine value final Map<String, Object> properties = new LinkedHashMap<String, Object>(); for (AnnotationAttribute annotationAttribute : annotationAttributeLists) { // find value pair for this attribute String attributeName = annotationAttribute.getName(); Pair<String, Object> pairFound = null; for (Pair<String, Object> annotationValuePair : desc.getAttributes()) { if (annotationValuePair.getFirst().equals(attributeName)) { pairFound = annotationValuePair; } } Object valueProvided = pairFound == null ? null : pairFound.getSecond(); Object value = getFinalValue(annotationClass, annotationAttribute, valueProvided, engineImportService); properties.put(attributeName, value); providedValues.remove(attributeName); requiredAttributes.remove(attributeName); } if (requiredAttributes.size() > 0) { List<String> required = new ArrayList<String>(requiredAttributes); Collections.sort(required); throw new AnnotationException("Annotation '" + annotationClass.getSimpleName() + "' requires a value for attribute '" + required.iterator().next() + "'"); } if (providedValues.size() > 0) { List<String> provided = new ArrayList<String>(providedValues); Collections.sort(provided); if (allAttributes.contains(provided.get(0))) { throw new AnnotationException("Annotation '" + annotationClass.getSimpleName() + "' has duplicate attribute values for attribute '" + provided.get(0) + "'"); } else { throw new AnnotationException("Annotation '" + annotationClass.getSimpleName() + "' does not have an attribute '" + provided.get(0) + "'"); } } // return handler InvocationHandler handler = new EPLAnnotationInvocationHandler(annotationClass, properties); return (Annotation) Proxy.newProxyInstance(engineImportService.getClassLoader(), new Class[]{annotationClass}, handler); } private static Object getFinalValue(Class annotationClass, AnnotationAttribute annotationAttribute, Object value, EngineImportService engineImportService) throws AnnotationException { if (value == null) { if (annotationAttribute.getDefaultValue() == null) { throw new AnnotationException("Annotation '" + annotationClass.getSimpleName() + "' requires a value for attribute '" + annotationAttribute.getName() + "'"); } return annotationAttribute.getDefaultValue(); } // handle non-array if (!annotationAttribute.getType().isArray()) { // handle primitive value if (!annotationAttribute.getType().isAnnotation()) { // if expecting an enumeration type, allow string value if (annotationAttribute.getType().isEnum() && JavaClassHelper.isImplementsInterface(value.getClass(), CharSequence.class)) { String valueString = value.toString().trim(); // find case-sensitive exact match first for (Object constant : annotationAttribute.getType().getEnumConstants()) { Enum e = (Enum) constant; if (e.name().equals(valueString)) { return constant; } } // find case-insensitive match String valueUppercase = valueString.toUpperCase(Locale.ENGLISH); for (Object constant : annotationAttribute.getType().getEnumConstants()) { Enum e = (Enum) constant; if (e.name().toUpperCase(Locale.ENGLISH).equals(valueUppercase)) { return constant; } } throw new AnnotationException("Annotation '" + annotationClass.getSimpleName() + "' requires an enum-value '" + annotationAttribute.getType().getSimpleName() + "' for attribute '" + annotationAttribute.getName() + "' but received '" + value + "' which is not one of the enum choices"); } // cast as required SimpleTypeCaster caster = SimpleTypeCasterFactory.getCaster(value.getClass(), annotationAttribute.getType()); Object finalValue = caster.cast(value); if (finalValue == null) { throw new AnnotationException("Annotation '" + annotationClass.getSimpleName() + "' requires a " + annotationAttribute.getType().getSimpleName() + "-typed value for attribute '" + annotationAttribute.getName() + "' but received " + "a " + value.getClass().getSimpleName() + "-typed value"); } return finalValue; } else { // nested annotation if (!(value instanceof AnnotationDesc)) { throw new AnnotationException("Annotation '" + annotationClass.getSimpleName() + "' requires a " + annotationAttribute.getType().getSimpleName() + "-typed value for attribute '" + annotationAttribute.getName() + "' but received " + "a " + value.getClass().getSimpleName() + "-typed value"); } return createProxy((AnnotationDesc) value, engineImportService); } } if (!value.getClass().isArray()) { throw new AnnotationException("Annotation '" + annotationClass.getSimpleName() + "' requires a " + annotationAttribute.getType().getSimpleName() + "-typed value for attribute '" + annotationAttribute.getName() + "' but received " + "a " + value.getClass().getSimpleName() + "-typed value"); } Object array = Array.newInstance(annotationAttribute.getType().getComponentType(), Array.getLength(value)); for (int i = 0; i < Array.getLength(value); i++) { Object arrayValue = Array.get(value, i); if (arrayValue == null) { throw new AnnotationException("Annotation '" + annotationClass.getSimpleName() + "' requires a " + "non-null value for array elements for attribute '" + annotationAttribute.getName() + "'"); } SimpleTypeCaster caster = SimpleTypeCasterFactory.getCaster(arrayValue.getClass(), annotationAttribute.getType().getComponentType()); Object finalValue = caster.cast(arrayValue); if (finalValue == null) { throw new AnnotationException("Annotation '" + annotationClass.getSimpleName() + "' requires a " + annotationAttribute.getType().getComponentType().getSimpleName() + "-typed value for array elements for attribute '" + annotationAttribute.getName() + "' but received " + "a " + arrayValue.getClass().getSimpleName() + "-typed value"); } Array.set(array, i, finalValue); } return array; } private static List<AnnotationAttribute> getAttributes(Class annotationClass) { List<AnnotationAttribute> props = new ArrayList<AnnotationAttribute>(); Method[] methods = annotationClass.getMethods(); if (methods == null) { return Collections.EMPTY_LIST; } for (int i = 0; i < methods.length; i++) { if (methods[i].getReturnType() == void.class) { continue; } if (methods[i].getParameterTypes().length > 0) { continue; } if ((methods[i].getName().equals("class")) || (methods[i].getName().equals("getClass")) || (methods[i].getName().equals("toString")) || (methods[i].getName().equals("annotationType")) || (methods[i].getName().equals("hashCode"))) { continue; } props.add(new AnnotationAttribute(methods[i].getName(), methods[i].getReturnType(), methods[i].getDefaultValue())); } Collections.sort(props, new Comparator<AnnotationAttribute>() { public int compare(AnnotationAttribute o1, AnnotationAttribute o2) { return o1.getName().compareTo(o2.getName()); } }); return props; } public static Annotation findAnnotation(Annotation[] annotations, Class annotationClass) { if (!annotationClass.isAnnotation()) { throw new IllegalArgumentException("Class " + annotationClass.getName() + " is not an annotation class"); } if (annotations == null || annotations.length == 0) { return null; } for (Annotation anno : annotations) { if (JavaClassHelper.isImplementsInterface(anno.getClass(), annotationClass)) { return anno; } } return null; } public static List<Annotation> findAnnotations(Annotation[] annotations, Class annotationClass) { if (!annotationClass.isAnnotation()) { throw new IllegalArgumentException("Class " + annotationClass.getName() + " is not an annotation class"); } if (annotations == null || annotations.length == 0) { return null; } List<Annotation> annotationsList = new ArrayList<Annotation>(); for (Annotation anno : annotations) { if (JavaClassHelper.isImplementsInterface(anno.getClass(), annotationClass)) { annotationsList.add(anno); } } return annotationsList; } public static Annotation[] mergeAnnotations(Annotation[] first, Annotation[] second) { return (Annotation[]) CollectionUtil.addArrays(first, second); } public static String getExpectSingleStringValue(String msgPrefix, List<AnnotationDesc> annotationsSameName) throws ExprValidationException { if (annotationsSameName.size() > 1) { throw new ExprValidationException(msgPrefix + " multiple annotations provided named '" + annotationsSameName.get(0).getName() + "'"); } AnnotationDesc annotation = annotationsSameName.get(0); Object value = AnnotationUtil.getValue(annotation); if (value == null) { throw new ExprValidationException(msgPrefix + " no value provided for annotation '" + annotation.getName() + "', expected a value"); } if (!(value instanceof String)) { throw new ExprValidationException(msgPrefix + " string value expected for annotation '" + annotation.getName() + "'"); } return (String) value; } }