/*
* Copyright (C) 2011 Peransin Nicolas.
* Use is subject to license terms.
*/
package org.mypsycho.beans;
import java.beans.PropertyDescriptor;
import java.lang.ref.Reference;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;
import org.apache.commons.beanutils.expression.Resolver;
/**
* This is the content of elements to inject into a bean.
*
* @author Peransin Nicolas
*/
public class Injection {
static final Object NULL_VALUE = new Object();
// For convenience, we ignore warning from attribute with upper-cased initial.
// It is a basic way to distinguish constant (not injectable property) from attribute.
public static final Pattern ATTRIBUT_PATTERN = Pattern.compile("[a-z_]\\w*");
public enum Nature {
SIMPLE, INDEXED, MAPPED
}
static final Nature[] NATURES = Nature.values();
// ArrayList is explicitly used to save memory (trim)
ArrayList<Injection> children = new ArrayList<Injection>();
Nature childrenNature = null;
// Key and definition are null at the root level
final Injection parent;
final Nature nature;
final Object id;
// How to create the value !
String definition = null;
int size = -1;
Reference<?> cache = null;
public Injection(Injection descriptor, Nature kind, Object key) {
parent = descriptor;
id = key;
nature = kind;
}
public Injection(InjectDescriptor descriptor) {
this(descriptor, null, null);
}
public Locale getLocale() {
return parent.getLocale();
}
public Injection getPath(String path, boolean init) throws IllegalArgumentException {
if (path == null || path.length() == 0) {
return this;
}
// String prop = getResolver().next(path);
String prop = getResolver().getProperty(path);
Nature childNature = Nature.SIMPLE;
Object childKey = prop;
String tail = getResolver().remove(path);
if (prop.length() != 0) {
tail = path.substring(prop.length());
if (tail.startsWith(".")) {
tail = tail.substring(1);
}
} else if (getResolver().isIndexed(path)) {
childKey = getResolver().getIndex(path);
childNature = Nature.INDEXED;
} else if (getResolver().isMapped(path)) {
childKey = getResolver().getKey(path);
childNature = Nature.MAPPED;
} else {
throw new IllegalArgumentException("Invalide path");
}
Injection child = getChild(childNature, childKey);
if (child == null) {
if (!init) {
return null;
}
child = createInjection(this, childNature, childKey);
children.add(child);
}
return child.getPath(tail, init);
}
/**
* Returns the parent.
*
* @return the parent
*/
public Injection getParent() {
return parent;
}
/**
* Returns the nature.
*
* @return the nature
*/
public Nature getNature() {
return nature;
}
/**
* Returns the id.
*
* @return the id
*/
public Object getId() {
return id;
}
protected Injector getInjector() {
return parent.getInjector();
}
protected Resolver getResolver() {
return getInjector().getResolver();
}
protected Invoker getInvoker() {
return getInjector().getInvoker();
}
protected InjectDescriptor getDescriptor() {
return parent.getDescriptor();
}
protected Injection getChild(Nature kind, Object id) {
if (children == null) {
return null;
}
for (Injection child : children) {
if ((kind == child.nature) && id.equals(child.id)) {
return child;
}
}
return null;
}
protected Injection createInjection(Injection container, Injection.Nature kind, Object id) {
return parent.createInjection(container, kind, id);
}
public String getCanonicalName() {
String path = parent.getCanonicalName();
switch (nature) {
case INDEXED:
return path + "[" + id + "]";
case MAPPED:
return path + "(" + id + ")";
case SIMPLE:
default:
return path + (path.endsWith("#") ? "" : ".") + id;
}
}
/**
* Compile the property.
* <p>
* Interpret templates.
* Remove deprecated child.
* Identify maximum index for indexed property.
* Compile and sort children (sort is required for indexed elements).
* </p>
*
* @return
*/
protected String compile() {
if (definition != null) {
expandTemplate();
}
for (Iterator<Injection> iChild = children.iterator(); iChild.hasNext();) {
Injection child = iChild.next();
if (getInjector().getDeprecated().equals(child.definition)) {
iChild.remove();
continue;
}
if (child.nature == Nature.INDEXED) {
size = Math.max(((Integer) child.id) + 1, size);
}
if (childrenNature == null) {
childrenNature = child.nature;
} else if (childrenNature != child.nature) {
childrenNature = Nature.SIMPLE;
}
String rejection = child.compile();
if (rejection != null) {
return rejection;
}
}
if (children.isEmpty()) {
children = null;
} else {
children.trimToSize();
// Sort indexed value so add function can be supported as extension
Collections.sort(children, new Comparator<Injection>() {
@Override
public int compare(Injection o1, Injection o2) {
int compare = o1.nature.compareTo(o2.nature);
if (compare != 0) {
return compare;
}
// String and Integer are comparable
return compare((Comparable<?>) o1.id, o2.id);
}
@SuppressWarnings("unchecked")
<T> int compare(Comparable<T> o1, Object o2) {
return o1.compareTo((T) o2);
}
});
}
return null;
}
/**
* Expand a template if the definition is a template.
*/
private void expandTemplate() {
InjectionTemplate call = getInjector().getTemplate().parse(definition);
if (call == null) {
return;
}
// To have shared templater
Injection template = getDescriptor().getPath(call.getName(), false);
if (template != null) {
// Check recursivity
definition = call.getValue();
// Definition must be update before check to break recursivity
for (Injection branch = this; branch != null; branch = branch.getParent()) {
if (template == branch) {
getInjector().notify(getCanonicalName(), "Recursive template", null);
return;
}
}
mergeTemplate(this, template, call);
expandTemplate(); // Template may be a template itself
} else {
getInjector().notify(getCanonicalName(), "Undefined template", null);
}
}
/**
* Merge a injection with a template.
* <p>
* Elements of the templates are added unless already defined.
* </p>
*
* @param injection
* @param template
* @param call
*/
private void mergeTemplate(Injection injection, Injection template, InjectionTemplate call) {
if (injection.definition == null) {
injection.definition = call.substitut(template.definition);
}
if (template.children == null) {
return;
}
for (Injection templateChild : template.children) {
String childPath = null;
switch (templateChild.nature) {
case SIMPLE:
childPath = (String) templateChild.getId();
break;
case INDEXED:
childPath = "[" + templateChild.getId() + "]";
break;
case MAPPED:
childPath = "(" + templateChild.getId() + ")";
break;
default:
break;
}
mergeTemplate(injection.getPath(childPath, true), templateChild, call);
}
}
public boolean isCollection() {
return isCollection(Nature.INDEXED) || isCollection(Nature.MAPPED);
}
public boolean isCollection(Nature n) {
return (definition == null) && (childrenNature == n);
}
/**
* Inject the content of this branch into the bean.
*
* @param type the type of collection if applicable
* @param bean the object of inject
* @param context injection context
*/
public void inject(Class<?> type, Object bean, InjectionContext context) {
try {
switch (nature) {
case SIMPLE:
injectSimple(bean, context);
break;
case MAPPED:
case INDEXED:
injectCollection(type, bean, context);
break;
default:
throw new IllegalStateException("Unexpected nature " + nature);
}
} catch (NoSuchMethodException e) {
Inject inject = bean.getClass().getAnnotation(Inject.class);
if ((inject != null) && inject.deferred().length > 0) {
if (Arrays.asList(inject.deferred()).contains(id)) {
return;
}
}
if (ATTRIBUT_PATTERN.matcher(""+id).matches()) {
getInjector().notify(getCanonicalName(), e.getMessage(), e);
}
} catch (Exception e) {
Throwable cause = e;
while (cause instanceof InvocationTargetException) {
cause = ((InvocationTargetException) cause).getTargetException();
}
if (cause instanceof Error) {
throw (Error) cause;
}
getInjector().notify(getCanonicalName(), cause.getMessage(), cause);
}
}
private void injectCollection(Class<?> type, Object bean, InjectionContext context) {
Object value = null;
if (definition != null) {
value = convert(type, bean, context);
}
if (nature == Nature.MAPPED) {
String key = (String) id;
// invoker.setCollection(value, key, value)
// invoker.getCollection(value, key)
if (value != null) {
getInvoker().setMapped(bean, key, value != NULL_VALUE ? value : null);
} else {
value = getInvoker().getMapped(bean, key);
}
} else if (nature == Nature.INDEXED) {
int index = (Integer) id;
if (value != null) {
getInvoker().setIndexed(bean, index, value != NULL_VALUE ? value : null);
} else {
value = getInvoker().getIndexed(bean, index);
}
} else { // impossible
throw new UnsupportedOperationException("Invalid nature " + nature);
}
if (!hasChildren() || value == NULL_VALUE) {
return;
}
if (value == null) {
type = fixImplicitCollection(type);
if (type == null) {
getInjector().notify("nullType", getCanonicalName(), null);
return;
}
String dimension = null;
if (isCollection(Nature.INDEXED)) {
dimension = String.valueOf(size);
}
value = convert(type, dimension, bean, context);
if (nature == Nature.MAPPED) {
String key = (String) id;
getInvoker().setMapped(bean, key, value);
} else if (nature == Nature.INDEXED) {
int index = (Integer) id;
getInvoker().setIndexed(bean, index, value);
}
}
if (value != null) {
injectChildren(getInvoker().getCollectedType(value.getClass()), value, context);
}
}
Class<?> fixImplicitCollection(Class<?> type) {
if (Object.class.equals(type)) {
if (isCollection(Nature.INDEXED)) {
return List.class;
} else if (isCollection(Nature.MAPPED)) {
return Map.class;
}
}
return type;
}
/**
* Do something TODO.
* <p>
* Details of the function.
* </p>
*
* @param bean
*/
private void injectSimple(Object bean, InjectionContext context)
throws IllegalAccessException, InvocationTargetException, NoSuchMethodException,
IllegalArgumentException {
PropertyDescriptor descr = getInjector().getPropertyDescriptor(bean, (String) id);
Object value = null;
// setter is performed once the bean is complete
Class<?> targetType = null;
boolean toSet = false;
if (definition != null) {
if (getInvoker().isWriteable(bean, descr, false)) {
targetType = getInvoker().getPropertyType(descr, false);
value = convert(targetType, bean, context);
toSet = true;
} else {
getInjector().notify("Undefined", getCanonicalName(), null);
return;
}
} else if (getInvoker().isReadable(bean, descr, false)) { // Property is accessible
value = getInvoker().get(bean, descr);
if (value == null) {
if (getInvoker().isWriteable(bean, descr, false)) {
targetType = getInvoker().getPropertyType(descr, false);
String dimension = null;
if (isCollection(Nature.INDEXED)) {
dimension = String.valueOf(size);
}
// instantiate
value = convert(targetType, dimension, bean, context);
toSet = true;
} else {
// getInjector().notify("Null ", getCanonicalName(), null); //
return;
}
}
} else if (getInvoker().isWriteable(bean, descr, false)) {
targetType = getInvoker().getPropertyType(descr, false);
String dimension = null;
if (isCollection(Nature.INDEXED)) {
dimension = String.valueOf(size);
}
// instantiate
value = convert(targetType, dimension, bean, context);
toSet = true;
}
if (value != null) {
if (value != NULL_VALUE) {
injectChildren(getInvoker().getCollectedType(value.getClass()), value, context);
}
if (toSet) { // setter is performed once the bean is complete
getInvoker().set(bean, descr, value != NULL_VALUE ? value : null);
}
return;
} else if (toSet) {
if (!definition.isEmpty()) {
throw new NullPointerException("'" + definition + "' has been converted as null");
}
return;
}
// Property is wrapped, value is not defined
if (getInvoker().isCollection(descr)) {
targetType = getInvoker().getPropertyType(descr, true);
}
Boolean childWritable = null;
for (Injection child : children) {
if (child.nature == Nature.SIMPLE) {
getInjector().notify("Inaccessible", child.getCanonicalName(), null);
continue;
}
Object childValue = null;
toSet = false;
if (child.definition != null) {
if (childWritable == null) { // Unique check for writable
childWritable = true;
if (targetType == null) {
getInjector().notify("Untyped", getCanonicalName(), null);
childWritable = false;
} else if (!getInvoker().isWriteable(bean, descr, true)) {
getInjector().notify("Unwrittable", getCanonicalName(), null);
childWritable = false;
}
}
if (childWritable) {
childValue = child.convert(child.fixImplicitCollection(targetType), bean, context);
toSet = true;
}
} else {
if (child.nature == Nature.MAPPED) {
childValue = getInvoker().get(bean, descr, (String) child.id);
} else if (child.nature == Nature.INDEXED) {
childValue = getInvoker().get(bean, descr, (Integer) child.id);
}
if (childValue == null) {
getInjector().notify("Null element", getCanonicalName(), null);
continue;
}
}
if (childValue != null) {
child.injectChildren(targetType, childValue, context);
if (toSet) {
if (child.nature == Nature.MAPPED) {
getInvoker().set(bean, descr, (String) child.id, childValue);
} else if (child.nature == Nature.INDEXED) {
getInvoker().set(bean, descr, (Integer) child.id, childValue);
}
}
} else if (toSet && !child.definition.isEmpty()) {
getInjector().notify(getCanonicalName(),
"'" + child.definition + "' has been converted as null", null);
}
}
}
/**
* Do something TODO.
* <p>
* Details of the function.
* </p>
*
* @return
*/
public boolean hasChildren() {
return children != null && !children.isEmpty();
}
protected void injectChildren(Class<?> type, Object value, InjectionContext context) {
if ((value == null) || (children == null)) {
return;
}
List<Injection> injections = children;
// Fix injection order
Inject inject = value.getClass().getAnnotation(Inject.class);
if ((inject != null) && inject.order().length > 0) {
injections = new ArrayList<Injection>(children.size());
for (String name : inject.order()) {
for (Injection child : children) {
if (name.equals(child.id)) {
injections.add(child);
break; // next order
}
}
}
for (Injection child : children) {
if (!injections.contains(child)) {
injections.add(child);
}
}
}
context.update(type, this, value);
for (Injection child : injections) {
child.inject(type, value, context);
}
if (value instanceof Injectable) {
// Nothing inject when no child
// Useless to create a context for something null
((Injectable) value).initResources(context.clone());
}
}
protected Object convert(Class<?> expected, Object parent, InjectionContext context)
throws IllegalArgumentException {
return convert(expected, definition, parent, context);
}
protected Object convert(Class<?> expected, String content, Object parent, InjectionContext context)
throws IllegalArgumentException {
Object value = (cache != null) ? cache.get() : null;
if (value != null) {
return value;
}
if (getInjector().getNullTag().equals(content)) {
return NULL_VALUE;
}
context.update(expected, this, parent);
value = getInjector().getConverter().convert(expected, content, context);
if (value instanceof Reference) {
cache = (Reference<?>) value;
return cache.get();
}
return value;
}
protected boolean isCollection(Class<?> type) {
if (type == null) {
return false;
}
return type.isArray() || List.class.isAssignableFrom(type)
|| Map.class.isAssignableFrom(type);
}
@Override
public String toString() {
return getCanonicalName() + ((definition != null) ? "=" + definition : "");
}
}