org.mypsycho.beans.Injection.java Source code

Java tutorial

Introduction

Here is the source code for org.mypsycho.beans.Injection.java

Source

/*
 * 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 : "");
    }

}