blob: 3a9003642b72b0ece5e881d5acfd4ba328851451 [file] [log] [blame]
/*
* 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;
}
}
}