| /* |
| * 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.felix.scr.impl.inject.internal; |
| |
| import java.lang.annotation.Annotation; |
| import java.lang.reflect.Array; |
| import java.lang.reflect.Field; |
| import java.lang.reflect.InvocationHandler; |
| import java.lang.reflect.Method; |
| import java.lang.reflect.Modifier; |
| import java.lang.reflect.Proxy; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import org.apache.felix.scr.impl.helper.Coercions; |
| import org.osgi.framework.Bundle; |
| import org.osgi.service.component.ComponentException; |
| |
| public class Annotations |
| { |
| /** Capture all methods defined by the annotation interface */ |
| private static final Set<Method> ANNOTATION_METHODS = new HashSet<>(); |
| static |
| { |
| for(final Method m : Annotation.class.getMethods()) |
| { |
| ANNOTATION_METHODS.add(m); |
| } |
| } |
| |
| /** Constant for the single element method */ |
| private static final String VALUE_METHOD = "value"; |
| |
| /** Constant for the prefix constant. */ |
| private static final String PREFIX_CONSTANT = "PREFIX_"; |
| |
| /** |
| * Check whether the provided type is a single element annotation. |
| * A single element annotation has a method named "value" and all |
| * other annotation methods must have a default value. |
| * @param clazz The provided type |
| * @return {@code true} if the type is a single element annotation. |
| */ |
| static public boolean isSingleElementAnnotation(final Class<?> clazz) |
| { |
| boolean result = false; |
| if ( clazz.isAnnotation() ) |
| { |
| result = true; |
| boolean hasValue = false; |
| for ( final Method method: clazz.getMethods() ) |
| { |
| // filter out methods from Annotation |
| boolean isFromAnnotation = false; |
| for(final Method objMethod : ANNOTATION_METHODS) |
| { |
| if ( objMethod.getName().equals(method.getName()) |
| && Arrays.equals(objMethod.getParameterTypes(), method.getParameterTypes()) ) |
| { |
| isFromAnnotation = true; |
| break; |
| } |
| } |
| if ( isFromAnnotation ) |
| { |
| continue; |
| } |
| if ( VALUE_METHOD.equals(method.getName()) ) |
| { |
| hasValue = true; |
| continue; |
| } |
| if ( method.getDefaultValue() == null ) |
| { |
| result = false; |
| break; |
| } |
| |
| } |
| if ( result ) |
| { |
| result = hasValue; |
| } |
| |
| } |
| return result; |
| } |
| |
| static public String getPrefix(Class<?> clazz) |
| { |
| try |
| { |
| final Field f = clazz.getField(PREFIX_CONSTANT); |
| if ( Modifier.isStatic(f.getModifiers()) |
| && Modifier.isPublic(f.getModifiers()) |
| && Modifier.isFinal(f.getModifiers()) |
| && String.class.isAssignableFrom(f.getType())) |
| { |
| final Object value = f.get(null); |
| if ( value != null ) |
| { |
| return value.toString(); |
| } |
| } |
| } |
| catch ( final Exception ignore) |
| { |
| // ignore |
| } |
| return null; |
| } |
| |
| @SuppressWarnings("unchecked") |
| static public <T> T toObject(Class<T> clazz, Map<String, Object> props, Bundle b, boolean supportsInterfaces ) |
| { |
| final boolean isSingleElementAnn = isSingleElementAnnotation(clazz); |
| final String prefix = getPrefix(clazz); |
| final Map<String, Object> m = new HashMap<>(); |
| |
| final Map<String, Method> complexFields = new HashMap<>(); |
| for ( final Method method: clazz.getMethods() ) |
| { |
| final String name = method.getName(); |
| final String mapped; |
| if ( isSingleElementAnn && name.equals(VALUE_METHOD) ) |
| { |
| mapped = mapTypeNameToKey(clazz.getSimpleName()); |
| } |
| else |
| { |
| mapped = mapIdentifierToKey(name); |
| } |
| final String key = (prefix == null ? mapped : prefix.concat(mapped)); |
| |
| Object raw = props.get(key); |
| Class<?> returnType = method.getReturnType(); |
| Object cooked; |
| if ( returnType.isInterface() || returnType.isAnnotation()) |
| { |
| complexFields.put(key, method); |
| continue; |
| } |
| |
| try |
| { |
| if (returnType.isArray()) |
| { |
| Class<?> componentType = returnType.getComponentType(); |
| if (componentType.isInterface() || componentType.isAnnotation()) |
| { |
| complexFields.put(key, method); |
| continue; |
| } |
| cooked = coerceToArray(componentType, raw, b); |
| } |
| else |
| { |
| cooked = Coercions.coerce(returnType, raw, b); |
| } |
| } |
| catch (ComponentException e) |
| { |
| cooked = new Invalid(e); |
| } |
| m.put( name, cooked ); |
| } |
| if (!complexFields.isEmpty()) |
| { |
| if (supportsInterfaces ) |
| { |
| Map<String, List<Map<String, Object>>> nested = extractSubMaps(complexFields.keySet(), props); |
| for (Map.Entry<String, Method> entry: complexFields.entrySet()) |
| { |
| List<Map<String, Object>> proplist = nested.get(entry.getKey()); |
| if (proplist == null) |
| { |
| proplist = Collections.emptyList(); |
| } |
| Method method = entry.getValue(); |
| Class<?> returnType = method.getReturnType(); |
| if (returnType.isArray()) |
| { |
| Class<?> componentType = returnType.getComponentType(); |
| Object result = Array.newInstance(componentType, proplist.size()); |
| for (int i = 0; i < proplist.size(); i++) |
| { |
| Map<String, Object> rawElement = proplist.get(i); |
| Object cooked = toObject(componentType, rawElement, b, supportsInterfaces); |
| Array.set(result, i, cooked); |
| } |
| m.put(method.getName(), result); |
| } |
| else |
| { |
| if (!proplist.isEmpty()) |
| { |
| Object cooked = toObject(returnType, proplist.get(0), b, supportsInterfaces); |
| m.put(method.getName(), cooked); |
| } |
| } |
| } |
| } |
| else |
| { |
| for (Method method: complexFields.values()) |
| { |
| m.put(method.getName(), new Invalid("Invalid annotation member type" + method.getReturnType().getName() + " for member: " + method.getName())); |
| } |
| } |
| } |
| |
| final InvocationHandler h = new Handler(m, clazz); |
| return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class<?>[] { clazz }, h); |
| } |
| |
| private static Map<String, List<Map<String, Object>>> extractSubMaps(Collection<String> keys, Map<String, Object> map) |
| { |
| Map<String, List<Map<String, Object>>> result = new HashMap<>(); |
| //Form a regexp to recognize all the keys as prefixes in the map keys. |
| StringBuilder b = new StringBuilder("("); |
| for (String key: keys) |
| { |
| b.append(key).append("|"); |
| } |
| b.deleteCharAt(b.length() -1); |
| b.append(")\\.([0-9]*)\\.(.*)"); |
| Pattern p = Pattern.compile(b.toString()); |
| for (Map.Entry<String, Object> entry: map.entrySet()) |
| { |
| String longKey = entry.getKey(); |
| Matcher m = p.matcher(longKey); |
| if (m.matches()) |
| { |
| String key = m.group(1); |
| int index = Integer.parseInt(m.group(2)); |
| String subkey = m.group(3); |
| List<Map<String, Object>> subMapsForKey = result.get(key); |
| if (subMapsForKey == null) |
| { |
| subMapsForKey = new ArrayList<>(); |
| result.put(key, subMapsForKey); |
| } |
| //make sure there is room for the possible new submap |
| for (int i = subMapsForKey.size(); i <= index; i++) |
| { |
| subMapsForKey.add(new HashMap<String, Object>()); |
| } |
| Map<String, Object> subMap = subMapsForKey.get(index); |
| subMap.put(subkey, entry.getValue()); |
| } |
| } |
| return result; |
| } |
| |
| private static Object coerceToArray(Class<?> componentType, Object raw, Bundle bundle) |
| { |
| if (raw == null) |
| { |
| return Array.newInstance(componentType, 0); |
| } |
| if (raw.getClass().isArray()) |
| { |
| int size = Array.getLength(raw); |
| Object result = Array.newInstance(componentType, size); |
| for (int i = 0; i < size; i++) |
| { |
| Object rawElement = Array.get(raw, i); |
| Object cooked = Coercions.coerce(componentType, rawElement, bundle); |
| Array.set(result, i, cooked); |
| } |
| return result; |
| } |
| if (raw instanceof Collection) |
| { |
| @SuppressWarnings("rawtypes") |
| Collection raws = (Collection) raw; |
| int size = raws.size(); |
| Object result = Array.newInstance(componentType, size); |
| int i = 0; |
| for (Object rawElement: raws) |
| { |
| Object cooked = Coercions.coerce(componentType, rawElement, bundle); |
| Array.set(result, i++, cooked); |
| } |
| return result; |
| |
| } |
| Object cooked = Coercions.coerce(componentType, raw, bundle); |
| Object result = Array.newInstance(componentType, 1); |
| Array.set(result, 0, cooked); |
| return result; |
| |
| } |
| |
| private static final Pattern p = Pattern.compile("(\\$_\\$)|(\\$\\$)|(\\$)|(__)|(_)"); |
| |
| static String mapIdentifierToKey(String name) |
| { |
| Matcher m = p.matcher(name); |
| StringBuffer b = new StringBuffer(); |
| while (m.find()) |
| { |
| String replacement = ""; |
| if (m.group(1) != null) replacement = "-"; |
| if (m.group(2) != null) replacement = "\\$"; |
| if (m.group(3) != null) replacement = ""; |
| if (m.group(4) != null) replacement = "_"; |
| if (m.group(5) != null) replacement = "."; |
| |
| m.appendReplacement(b, replacement); |
| } |
| m.appendTail(b); |
| return b.toString(); |
| } |
| |
| static String mapTypeNameToKey(String name) |
| { |
| final StringBuilder sb = new StringBuilder(); |
| boolean lastLow = false; |
| for(final char c : name.toCharArray()) |
| { |
| if ( lastLow && (Character.isLetter(c) || Character.isDigit(c)) && Character.isUpperCase(c) ) |
| { |
| sb.append('.'); |
| } |
| lastLow = false; |
| if ( (Character.isLetter(c) || Character.isDigit(c)) && Character.isLowerCase(c)) |
| { |
| lastLow = true; |
| } |
| sb.append(Character.toLowerCase(c)); |
| } |
| return sb.toString(); |
| } |
| |
| private final static class Handler implements InvocationHandler |
| { |
| private final Map<String, Object> values; |
| |
| private final Class<?> type; |
| |
| public Handler(final Map<String, Object> values, final Class<?> type) |
| { |
| this.values = values; |
| this.type = type; |
| } |
| |
| @Override |
| public Object invoke(Object proxy, Method method, Object[] args) throws Throwable |
| { |
| Object value = values.get(method.getName()); |
| if (value instanceof Invalid) |
| { |
| throw new ComponentException(((Invalid)value).getMessage()); |
| } |
| if ( value == null ) |
| { |
| // check for methods of the Annotations class like hashCode, toString, equals etc. |
| if (method.getName().equals("hashCode") && |
| method.getParameterTypes().length == 0 ) |
| { |
| int hashCode = 0; |
| for (final Map.Entry<String, Object> entry : values.entrySet()) { |
| if (value instanceof Invalid) { |
| continue; |
| } |
| hashCode += (127 * entry.getKey().hashCode()) ^ entry.getValue().hashCode(); |
| } |
| value = hashCode; |
| } |
| else if (method.getName().equals("equals") |
| && method.getParameterTypes().length == 1) |
| { |
| final Object other = args[0]; |
| if (proxy == other) |
| { |
| value = true; |
| } |
| else |
| { |
| value = false; |
| if (type.isInstance(other) && Proxy.isProxyClass(other.getClass())) |
| { |
| final InvocationHandler ih = Proxy.getInvocationHandler(other); |
| if (ih instanceof Handler) { |
| value = ((Handler)ih).values.equals(values); |
| } |
| } |
| } |
| } |
| else if (method.getName().equals("toString") |
| && method.getParameterTypes().length == 0 ) |
| { |
| value = type.getName() + " : " + values; |
| } |
| else if (method.getName().equals("annotationType") |
| && method.getParameterTypes().length == 0 ) |
| { |
| value = type; |
| } |
| } |
| return value; |
| } |
| } |
| |
| private final static class Invalid |
| { |
| private final String message; |
| |
| public Invalid(ComponentException e) |
| { |
| this.message = e.getMessage(); |
| } |
| |
| public Invalid(String message) |
| { |
| this.message = message; |
| } |
| |
| public String getMessage() |
| { |
| return message; |
| } |
| } |
| |
| } |