blob: 4a88faeab409e7a522bb484dcf69ca1f60aee225 [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.sigil.common.model;
import java.io.Serializable;
import java.lang.ref.SoftReference;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.logging.Logger;
import org.apache.felix.sigil.common.model.annotations.Required;
public class ModelElementSupport implements Serializable
{
private static final Logger log = Logger.getLogger(ModelElementSupport.class.getName());
private static final long serialVersionUID = 1L;
private static final PropertyAdapter[] EMPTY_PROPS = new PropertyAdapter[] {};
private static final IModelElement[] EMPTY_ELEMENTS = new IModelElement[] {};
private static final Object[] ZERO_ARGS = new Object[] {};
private static final Class<?>[] ZERO_PARAMS = new Class[] {};
private static final WeakHashMap<Class<?>, SoftReference<ChildAdapter[]>> adapterCache = new WeakHashMap<Class<?>, SoftReference<ChildAdapter[]>>();;
private static final WeakHashMap<Class<?>, SoftReference<PropertyAdapter[]>> propertyCache = new WeakHashMap<Class<?>, SoftReference<PropertyAdapter[]>>();;
private IModelElement target;
private transient SoftReference<PropertyAdapter[]> propertyReference;
private transient SoftReference<ChildAdapter[]> childrenReference;
private transient SoftReference<Set<String>> propertyNameReference;
public ModelElementSupport(IModelElement target)
{
this.target = target;
}
public void setProperty(String name, Object value) throws NoSuchMethodException
{
PropertyAdapter p = findProperty(name, value);
if (p == null)
{
throw new NoSuchMethodException("No such property " + name + " for type "
+ target.getClass());
}
invoke(target, p.getWriteMethod(), value);
}
public void addProperty(String name, Object value) throws NoSuchMethodException
{
PropertyAdapter p = findProperty(name, value);
if (p == null)
{
throw new NoSuchMethodException("No such property " + name + " for type "
+ target.getClass());
}
invoke(target, p.getAddMethod(), value);
}
public void removeProperty(String name, Object value) throws NoSuchMethodException
{
PropertyAdapter p = findProperty(name, value);
if (p == null)
{
throw new NoSuchMethodException("No such property " + name + " for type "
+ target.getClass());
}
invoke(target, p.getRemoveMethod(), value);
}
public Object getProperty(String name) throws NoSuchMethodException
{
PropertyAdapter p = findProperty(name, null);
if (p == null)
{
throw new NoSuchMethodException("No such property " + name + " for type "
+ target.getClass());
}
return invoke(target, p.getReadMethod(), ZERO_ARGS);
}
public Set<String> getPropertyNames()
{
Set<String> names = propertyNameReference == null ? null
: propertyNameReference.get();
if (names == null)
{
names = new HashSet<String>();
PropertyAdapter[] props = cachedProps(target.getClass());
for (PropertyAdapter prop : props)
{
names.add(prop.getName());
}
propertyNameReference = new SoftReference<Set<String>>(names);
}
return names;
}
public Object getDefaultPropertyValue(String name)
{
try
{
Method m = target.getClass().getMethod(makeDefaultPropertyValue(name),
ZERO_PARAMS);
return invoke(target, m, ZERO_ARGS);
}
catch (SecurityException e)
{
throw new UndeclaredThrowableException(e);
}
catch (NoSuchMethodException e)
{
// fine no default
return null;
}
}
public Class<?> getPropertyType(String name) throws NoSuchMethodException
{
PropertyAdapter p = findProperty(name, null);
if (p == null)
{
throw new NoSuchMethodException("No such property " + name + " for type "
+ target.getClass());
}
return p.getPropertyType();
}
@SuppressWarnings("unchecked")
public <T extends IModelElement> T[] childrenOfType(Class<T> type)
{
ChildAdapter[] adapters = cachedAdapters();
if (adapters.length == 0)
{
// return (T[]) EMPTY_ELEMENTS;
return ((T[]) Array.newInstance(type, 0));
}
ArrayList<T> elements = new ArrayList<T>();
for (ChildAdapter adapter : adapters)
{
Collection<? extends IModelElement> val = adapter.members(target);
for (IModelElement e : val)
{
if (type.isInstance(e))
{
elements.add((T) e);
}
}
}
//return elements.toArray( (T[]) EMPTY_ELEMENTS );
return elements.toArray((T[]) Array.newInstance(type, elements.size()));
}
public IModelElement[] children()
{
ChildAdapter[] adapters = cachedAdapters();
if (adapters.length == 0)
{
return EMPTY_ELEMENTS;
}
ArrayList<IModelElement> elements = new ArrayList<IModelElement>();
for (ChildAdapter adapter : adapters)
{
elements.addAll(adapter.members(target));
}
return elements.toArray(EMPTY_ELEMENTS);
}
public boolean addChild(IModelElement element) throws InvalidModelException
{
if (element.getParent() == null)
{
ChildAdapter[] adapters = cachedAdapters();
if (adapters.length > 0)
{
for (ChildAdapter adapter : adapters)
{
if (adapter.add(target, element))
{
element.setParent(target);
return true;
}
}
}
}
return false;
}
public boolean removeChild(IModelElement element)
{
if (element.getParent() == target)
{
ChildAdapter[] adapters = cachedAdapters();
if (adapters.length > 0)
{
for (ChildAdapter adapter : adapters)
{
if (adapter.remove(target, element))
{
element.setParent(null);
return true;
}
}
}
}
return false;
}
public Set<Class<? extends IModelElement>> getChildrenTypes(boolean required)
{
ChildAdapter[] adapters = cachedAdapters();
if (adapters.length == 0)
{
return Collections.emptySet();
}
HashSet<Class<? extends IModelElement>> types = new HashSet<Class<? extends IModelElement>>();
for (ChildAdapter adapter : adapters)
{
if (adapter.isRequired() == required)
{
Class<? extends IModelElement> type = adapter.getType();
if (type != null)
{
types.add(type);
}
}
}
return types;
}
private PropertyAdapter findProperty(String name, Object value)
{
PropertyAdapter[] props = propertyReference == null ? null
: propertyReference.get();
if (props == null)
{
props = cachedProps(target.getClass());
propertyReference = new SoftReference<PropertyAdapter[]>(props);
}
for (PropertyAdapter prop : props)
{
if (prop.getName().equals(name)
&& (value == null || prop.getRawType().isAssignableFrom(value.getClass())))
{
return prop;
}
}
return null;
}
private static synchronized PropertyAdapter[] cachedProps(
Class<? extends IModelElement> type)
{
SoftReference<PropertyAdapter[]> ref = propertyCache.get(type);
PropertyAdapter[] props = ref == null ? null : ref.get();
if (props == null)
{
props = loadProps(type);
propertyCache.put(type, new SoftReference<PropertyAdapter[]>(props));
}
return props;
}
private static PropertyAdapter[] loadProps(Class<? extends IModelElement> type)
{
ArrayList<PropertyAdapter> props = new ArrayList<PropertyAdapter>();
for (Method m : type.getMethods())
{
if (isValidProperty(m))
{
try
{
PropertyAdapter p = new PropertyAdapter(m, type);
props.add(p);
}
catch (NoSuchMethodException e)
{
// fine not a bean method
log.finer("Invalid bean property method " + m + ": " + e.getMessage());
}
}
}
return props.toArray(EMPTY_PROPS);
}
private static boolean isValidProperty(Method m)
{
return m.getName().startsWith("get") && m.getParameterTypes().length == 0
&& !m.getDeclaringClass().equals(Object.class)
&& !IModelElement.class.isAssignableFrom(m.getReturnType());
}
private static String makeDefaultPropertyValue(String name)
{
return "getDefault" + capitalise(name);
}
private static String capitalise(String name)
{
return Character.toUpperCase(name.charAt(0)) + name.substring(1);
}
private static String decapitalise(String substring)
{
return Character.toLowerCase(substring.charAt(0)) + substring.substring(1);
}
private ChildAdapter[] cachedAdapters()
{
ChildAdapter[] adapters = childrenReference == null ? null
: childrenReference.get();
if (adapters == null)
{
adapters = loadAdapters(target);
childrenReference = new SoftReference<ChildAdapter[]>(adapters);
}
return adapters;
}
private static synchronized ChildAdapter[] loadAdapters(IModelElement target)
{
Class<? extends IModelElement> type = target.getClass();
SoftReference<ChildAdapter[]> ref = adapterCache.get(type);
ChildAdapter[] adapters = ref == null ? null : ref.get();
if (adapters == null)
{
adapters = buildAdapters(type);
adapterCache.put(type, new SoftReference<ChildAdapter[]>(adapters));
}
return adapters;
}
private static ChildAdapter[] buildAdapters(Class<? extends IModelElement> type)
{
ArrayList<ChildAdapter> adapters = new ArrayList<ChildAdapter>();
for (Method m : type.getMethods())
{
ChildAdapter adapter = null;
if (isValidGetProperty(m))
{
adapter = buildGetAdapter(m);
}
else if (isValidSetProperty(m))
{
adapter = buildSetAdapter(m);
}
else if (isValidAddProperty(m))
{
adapter = buildAddAdapter(m);
}
else if (isValidRemoveProperty(m))
{
adapter = buildRemoveAdapter(m);
}
if (adapter != null)
{
adapters.add(adapter);
}
}
return adapters.toArray(new ChildAdapter[adapters.size()]);
}
private static ChildAdapter buildGetAdapter(Method m)
{
if (IModelElement.class.isAssignableFrom(m.getReturnType()))
{
return new GetPropertyAdapter(m);
}
else if (Collection.class.isAssignableFrom(m.getReturnType()))
{
return new GetCollectionAdapter(m);
}
else if (isModelArray(m.getReturnType()))
{
return new GetArrayAdapter(m);
}
else
{
return null;
}
}
private static ChildAdapter buildSetAdapter(Method m)
{
if (IModelElement.class.isAssignableFrom(m.getParameterTypes()[0]))
{
return new SetPropertyAdapter(m);
}
else
{
return null;
}
}
private static ChildAdapter buildAddAdapter(Method m)
{
if (IModelElement.class.isAssignableFrom(m.getParameterTypes()[0]))
{
return new AddPropertyAdapter(m);
}
else
{
return null;
}
}
private static ChildAdapter buildRemoveAdapter(Method m)
{
if (IModelElement.class.isAssignableFrom(m.getParameterTypes()[0]))
{
return new RemovePropertyAdapter(m);
}
else
{
return null;
}
}
private static boolean isValidRemoveProperty(Method m)
{
return m.getParameterTypes().length == 1 && m.getName().startsWith("remove")
&& !isDeclared(ICompoundModelElement.class, m);
}
private static boolean isValidAddProperty(Method m)
{
return m.getParameterTypes().length == 1 && m.getName().startsWith("add")
&& !isDeclared(ICompoundModelElement.class, m);
}
private static boolean isDeclared(Class<? extends IModelElement> element, Method m)
{
try
{
element.getMethod(m.getName(), m.getParameterTypes());
return true;
}
catch (SecurityException e)
{
throw new UndeclaredThrowableException(e);
}
catch (NoSuchMethodException e)
{
return false;
}
}
private static boolean isValidSetProperty(Method m)
{
return m.getParameterTypes().length == 1 && m.getName().startsWith("set")
&& !isDeclared(IModelElement.class, m);
}
private static boolean isValidGetProperty(Method m)
{
return m.getParameterTypes().length == 0 && m.getName().startsWith("get")
&& !isDeclared(IModelElement.class, m)
&& !isDeclared(ICompoundModelElement.class, m);
}
private static Object invoke(Object target, Method m, Object... args)
{
try
{
return m.invoke(target, args);
}
catch (IllegalArgumentException e)
{
// this should already have been tested
throw new IllegalStateException(e);
}
catch (IllegalAccessException e)
{
throw new UndeclaredThrowableException(e);
}
catch (InvocationTargetException e)
{
throw new UndeclaredThrowableException(e.getCause());
}
}
private static class PropertyAdapter
{
String prop;
String name;
Method g;
Method s;
Method a;
Method r;
Class<?> propertyType;
public PropertyAdapter(Method g, Class<?> type) throws SecurityException, NoSuchMethodException
{
if (g.getReturnType().isArray()
|| Iterable.class.isAssignableFrom(g.getReturnType()))
{
prop = g.getName().substring(3);
// remove trailing s - as in addWibble, removeWibble, getWibbles
prop = prop.substring(0, prop.length() - 1);
name = decapitalise(prop);
a = find("add", prop, g.getReturnType(), type);
propertyType = a.getParameterTypes()[0];
r = find("remove", prop, g.getReturnType(), type);
if (r.getParameterTypes()[0] != propertyType)
{
throw new NoSuchMethodException(
"Add remove property method types do not match");
}
propertyType = Array.newInstance(propertyType, 0).getClass();
}
else
{
prop = g.getName().substring(3);
name = decapitalise(prop);
propertyType = g.getReturnType();
s = find("set", prop, propertyType, type);
}
this.g = g;
}
public Class<?> getRawType()
{
return propertyType.isArray() ? propertyType.getComponentType()
: propertyType;
}
public Class<?> getPropertyType()
{
return propertyType;
}
public Method getReadMethod()
{
return g;
}
public Method getAddMethod() throws NoSuchMethodException
{
if (a == null)
{
throw new NoSuchMethodException("No such method add" + prop);
}
return a;
}
public Method getRemoveMethod() throws NoSuchMethodException
{
if (r == null)
{
throw new NoSuchMethodException("No such method remove" + prop);
}
return r;
}
public Method getWriteMethod() throws NoSuchMethodException
{
if (s == null)
{
throw new NoSuchMethodException("No such method set" + prop);
}
return s;
}
@Override
public String toString()
{
return "PropertyAdapter[" + name + "]";
}
private Method find(String prefix, String prop, Class<?> returnType, Class<?> type)
throws SecurityException, NoSuchMethodException
{
String methodName = prefix + prop;
if (returnType.isArray())
{
Class<?> t = returnType.getComponentType();
return type.getMethod(methodName, new Class[] { t });
}
else if (Iterable.class.isAssignableFrom(returnType))
{
Method f = null;
for (Method m : type.getMethods())
{
if (m.getParameterTypes().length == 1
&& m.getName().equals(methodName)
&& !IModelElement.class.isAssignableFrom(m.getParameterTypes()[0]))
{
if (f == null)
{
f = m;
}
else
{
throw new NoSuchMethodException("Found duplicate "
+ methodName);
}
}
}
if (f == null)
{
throw new NoSuchMethodException("No such method " + methodName);
}
return f;
}
else
{
return type.getMethod(methodName, new Class[] { returnType });
}
}
public String getName()
{
return name;
}
}
private static abstract class ChildAdapter
{
Method m;
ChildAdapter(Method m)
{
this.m = m;
}
public boolean isRequired()
{
return m.isAnnotationPresent(Required.class);
}
boolean add(Object target, IModelElement element)
{
return false;
}
abstract Class<? extends IModelElement> getType();
boolean remove(Object target, IModelElement element)
{
return false;
}
Collection<? extends IModelElement> members(Object target)
{
return Collections.emptyList();
}
@Override
public String toString()
{
return "ChildAdapter[ " + m.getName() + "]";
}
}
private static class GetPropertyAdapter extends ChildAdapter
{
GetPropertyAdapter(Method m)
{
super(m);
}
@Override
Collection<? extends IModelElement> members(Object target)
{
IModelElement member = (IModelElement) invoke(target, m, ZERO_ARGS);
if (member == null)
{
return Collections.emptyList();
}
else
{
return Collections.<IModelElement> singleton(member);
}
}
@SuppressWarnings("unchecked")
@Override
Class<? extends IModelElement> getType()
{
return (Class<? extends IModelElement>) m.getReturnType();
}
}
private static class GetCollectionAdapter extends ChildAdapter
{
public GetCollectionAdapter(Method m)
{
super(m);
}
@SuppressWarnings("unchecked")
@Override
Collection<? extends IModelElement> members(Object target)
{
Collection members = (Collection) invoke(target, m, ZERO_ARGS);
if (members == null)
{
return Collections.emptyList();
}
else
{
ArrayList<IModelElement> safe = new ArrayList<IModelElement>(
members.size());
for (Object o : members)
{
if (o instanceof IModelElement)
{
safe.add((IModelElement) o);
}
}
return safe;
}
}
@Override
Class<? extends IModelElement> getType()
{
// impossible to get type of a collection as erasure removes generics info
return null;
}
}
private static class GetArrayAdapter extends ChildAdapter
{
public GetArrayAdapter(Method m)
{
super(m);
}
@Override
Collection<? extends IModelElement> members(Object target)
{
IModelElement[] array = (IModelElement[]) invoke(target, m, ZERO_ARGS);
if (array == null || array.length == 0)
{
return Collections.emptyList();
}
else
{
return (Collection<? extends IModelElement>) Arrays.asList(array);
}
}
@SuppressWarnings("unchecked")
@Override
Class<? extends IModelElement> getType()
{
return (Class<? extends IModelElement>) m.getReturnType().getComponentType();
}
}
private static class SetPropertyAdapter extends ChildAdapter
{
public SetPropertyAdapter(Method m)
{
super(m);
}
@Override
boolean add(Object target, IModelElement element)
{
if (m.getParameterTypes()[0].isAssignableFrom(element.getClass()))
{
invoke(target, m, new Object[] { element });
return true;
}
else
{
return false;
}
}
@Override
boolean remove(Object target, IModelElement element)
{
if (m.getParameterTypes()[0].isAssignableFrom(element.getClass()))
{
invoke(target, m, new Object[] { null });
return true;
}
else
{
return false;
}
}
@SuppressWarnings("unchecked")
@Override
Class<? extends IModelElement> getType()
{
return (Class<? extends IModelElement>) m.getParameterTypes()[0];
}
}
private static class AddPropertyAdapter extends ChildAdapter
{
public AddPropertyAdapter(Method m)
{
super(m);
}
@Override
boolean add(Object target, IModelElement element)
{
if (m.getParameterTypes()[0].isAssignableFrom(element.getClass()))
{
invoke(target, m, new Object[] { element });
return true;
}
else
{
return false;
}
}
@SuppressWarnings("unchecked")
@Override
Class<? extends IModelElement> getType()
{
return (Class<? extends IModelElement>) m.getParameterTypes()[0];
}
}
private static class RemovePropertyAdapter extends ChildAdapter
{
public RemovePropertyAdapter(Method m)
{
super(m);
}
@Override
boolean remove(Object target, IModelElement element)
{
if (m.getParameterTypes()[0].isAssignableFrom(element.getClass()))
{
invoke(target, m, new Object[] { element });
return true;
}
else
{
return false;
}
}
@SuppressWarnings("unchecked")
@Override
Class<? extends IModelElement> getType()
{
return (Class<? extends IModelElement>) m.getParameterTypes()[0];
}
}
private static boolean isModelArray(Class<?> returnType)
{
return returnType.isArray()
&& IModelElement.class.isAssignableFrom(returnType.getComponentType());
}
}