kenh.xscript.impl.BaseElement.java Source code

Java tutorial

Introduction

Here is the source code for kenh.xscript.impl.BaseElement.java

Source

/*
 * xScript (XML Script Language)
 * Copyright 2015 and beyond, Kenneth Huang
 * 
 * This file is part of xScript.
 * 
 * xScript is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License version 3
 * as published by the Free Software Foundation.
 * 
 * xScript is distributed in the hope that it will be useful, 
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with xScript.  If not, see <http://www.gnu.org/licenses/>. 
 */

package kenh.xscript.impl;

import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.*;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import kenh.expl.UnsupportedExpressionException;
import kenh.xscript.*;
import kenh.xscript.annotation.*;
import kenh.xscript.elements.Script;

/**
 * Provide base function for all elements
 * 
 * @author Kenneth
 *
 */
public abstract class BaseElement implements Element {

    private Map<String, String> attributes = new LinkedHashMap(); // store attributes

    private Vector<Element> children = new Vector(); // store child elements

    private String text = null;

    private Environment env = null;

    private Element parent = null;

    /**
     * the attribute group that this element support
     * if element have two attribute group: (a, b), (a, c), then this element
     * is support a & c, not support b & c or a only.
     * 
     */
    private Vector<String[]> allAttributeAnnotation = null;

    protected static final Log logger = LogFactory.getLog(Element.class.getName());

    /**
     * Find attribute annotations for all process method
     */
    private void readAttributeAnnotation() {
        if (allAttributeAnnotation != null)
            return;

        allAttributeAnnotation = new Vector();

        Method[] methods = this.getClass().getMethods();

        for (Method method : methods) {
            String name = method.getName();
            Annotation process = method.getAnnotation(Processing.class);
            Annotation[][] annotations = method.getParameterAnnotations();

            if (process != null || name.equals(METHOD)) {

                boolean find = true;
                String[] group = new String[annotations.length];
                for (int i = 0; i < annotations.length; i++) {
                    Annotation[] anns = annotations[i];
                    if (anns == null) {
                        find = false;
                        break;
                    }
                    String attributeName = null;
                    for (Annotation ann : anns) {
                        if (ann instanceof Attribute) {
                            attributeName = ((Attribute) ann).value();
                            break;
                        }
                    }

                    if (StringUtils.isBlank(attributeName)) {
                        find = false;
                        break;
                    }

                    group[i] = attributeName;

                }
                if (find) {

                    allAttributeAnnotation.add(group);

                }
            }
        }
    }

    @Override
    public void setAttribute(String name, String value) throws UnsupportedScriptException {
        if (StringUtils.isBlank(name))
            throw new UnsupportedScriptException(this, "Attribute is empty.");
        if (attributes.containsKey(name))
            throw new UnsupportedScriptException(this, "Reduplicate attribute. [" + name + "]");

        attributes.put(name, value);

        readAttributeAnnotation();

        // Check attributes, if current attributes combination is suit for process method's attribute group.
        for (String[] group : allAttributeAnnotation) {
            int i = 0;
            for (String key : group) {

                if (attributes.containsKey(key)) {
                    i++;
                }
            }
            if (i == attributes.size())
                return;
        }

        Set<String> keys = attributes.keySet();
        String keyStr = "";
        for (String key : keys) {
            keyStr = keyStr + key + ",";
        }
        keyStr = StringUtils.substringBeforeLast(keyStr, ",");

        throw new UnsupportedScriptException(this, "Do not support this attribute group. [" + keyStr + "]");
    }

    @Override
    public String getAttribute(String name) {
        return attributes.get(name);
    }

    @Override
    public Map<String, String> getAttributes() {
        return attributes;
    }

    /**
     * Check child element is allowed to add.
     * @param e
     * @return
     */
    private boolean checkChildElement(Element e) {
        if (e == null)
            return false;

        Class class_ = e.getClass();

        if (isExcludedChildElement(this, e.getClass())) {
            return false;
        }

        if (isIncludeChildElement(this, e.getClass())) {
            return true;
        }

        return false;
    }

    /**
     * Check {@code Exclude} annotation, if child element's class is in exclude
     * list, this child element is not allowed to add.
     * If {@code Exclude} annotation do not define, all child element is allowed.
     * 
     * @param c
     * @return true  this child element is not allowed to add.
     */
    private static boolean isExcludedChildElement(Element el, Class c) {
        if (!isElement(c))
            return true; // if not an Element

        Exclude e = el.getClass().getAnnotation(Exclude.class);
        if (e == null)
            return false;

        Class[] classes = e.value();
        if (classes == null || classes.length <= 0)
            return false;

        for (Class class_ : classes) {
            if (class_.isAssignableFrom(c))
                return true;
        }

        return false;
    }

    /**
     * Check {@code Include} annotation, if child element's class is in include
     * list, this child element is allowed to add.
     * If {@code Include} annotation do not define, all child element is allowed.
     * 
     * @param c
     * @return true  this child element is allowed to add.
     */
    private static boolean isIncludeChildElement(Element el, Class c) {
        if (!isElement(c))
            return false; // if not an Element

        Include e = el.getClass().getAnnotation(Include.class);
        if (e == null)
            return true;

        Class[] classes = e.value();
        if (classes == null || classes.length <= 0)
            return true;

        int[] numbers = e.number();
        int i = 0;
        for (Class class_ : classes) {
            int number = 0;
            if (i < numbers.length)
                number = numbers[i];
            i++;
            if (class_.isAssignableFrom(c))
                return checkChildrenElementAmount(el, number, class_);
        }

        return false;
    }

    /**
     * Check amount of child element with certain class type.
     * 
     * @see kenh.xscript.annotation.Include#number()
     * @param number 
     * @param c
     * @return  true, this amount is less than {@code number}; false, this amount is more than {@code number}
     */
    private static boolean checkChildrenElementAmount(Element el, int number, Class c) {
        if (number <= 0)
            return true;

        int i = number;
        Vector<Element> children = el.getChildren();
        for (Element child : children) {
            if (c.isAssignableFrom(child.getClass()))
                i--;
        }

        if (i <= 0)
            return false;

        return true;
    }

    /**
     * Check element is a sub class of {@code Element}.
     * @param c
     * @return
     */
    private static boolean isElement(Class c) {
        if (c == null)
            return false;

        return Element.class.isAssignableFrom(c);
    }

    @Override
    public void addChild(Element child) throws UnsupportedScriptException {
        if (child == null)
            throw new UnsupportedScriptException(this, "Element is empty.");
        if (children.contains(child))
            throw new UnsupportedScriptException(this, "Reduplicate element. [" + child.toString() + "]");

        if (checkChildElement(child)) {
            children.add(child);
            child.setParent(this);
        } else {
            throw new UnsupportedScriptException(child,
                    "Do not support this child element. [" + child.getClass().getCanonicalName() + "]");
        }

    }

    @Override
    public Vector<Element> getChildren() {
        return children;
    }

    @Override
    public void setText(String text) throws UnsupportedScriptException {
        Text t = this.getClass().getAnnotation(Text.class);
        if (t != null && t.value() != Text.Type.NONE) {
            this.text = text;
            return;
        }
        throw new UnsupportedScriptException(this, "Do not support text.");
    }

    @Override
    public String getText() {
        Text t = this.getClass().getAnnotation(Text.class);
        if (t != null) {
            Text.Type type = t.value();
            if (type == Text.Type.FULL) {
                return text;
            } else if (type == Text.Type.TRIM) {
                return StringUtils.trimToEmpty(text);
            }

        }
        return null;
    }

    /**
     * Get the parsed text.
     * @return
     */
    protected Object getParsedText() throws UnsupportedScriptException {
        if (this.getText() == null)
            return "";

        String text = this.getText();
        Text t = this.getClass().getAnnotation(Text.class);
        if (t != null) {
            Text.Type type = t.value();

            if (type == Text.Type.FULL) {
                try {
                    return this.getEnvironment().parse(text);
                } catch (UnsupportedExpressionException e) {
                    throw new UnsupportedScriptException(this, e);
                }

            } else if (type == Text.Type.TRIM) {
                try {
                    text = StringUtils.trimToEmpty(text);
                    Object obj = this.getEnvironment().parse(text);
                    if (obj instanceof String) {
                        return StringUtils.trimToEmpty((String) obj);
                    } else {
                        return obj;
                    }
                } catch (UnsupportedExpressionException e) {
                    throw new UnsupportedScriptException(this, e);
                }
            }

            return "";
        } else {
            return "";
        }

    }

    @Override
    public void setEnvironment(Environment env) {
        this.env = env;
    }

    @Override
    public Environment getEnvironment() {
        return env;
    }

    /**
     * the name of default process method
     */
    private static final String METHOD = "process";

    @Override
    public int invoke() throws UnsupportedScriptException {

        logger.info(getInfo());

        Annotation ignoreSuperClass = this.getClass().getAnnotation(IgnoreSuperClass.class);

        Method[] methods = this.getClass().getMethods();
        if (ignoreSuperClass != null)
            methods = this.getClass().getDeclaredMethods();

        for (Method method : methods) {

            String name = method.getName();
            Class[] classes = method.getParameterTypes();
            Annotation primal = method.getAnnotation(Primal.class);
            Annotation process = method.getAnnotation(Processing.class);
            Annotation[][] annotations = method.getParameterAnnotations();

            if (process == null && !name.equals(METHOD))
                continue;

            boolean parsedAll = true;
            if (primal != null)
                parsedAll = false;

            boolean find = true; // true, find the suitable method to invoke

            Object[] objs = new Object[annotations.length];

            if (annotations.length == 0) { // non-parameter method
                if (attributes.size() == 0)
                    find = true;
                else {
                    logger.trace("Failure(no parameter required): " + method.toGenericString());
                    find = false;
                }
            } else {

                if (annotations.length != attributes.size())
                    continue;

                for (int i = 0; i < annotations.length; i++) {

                    boolean parsed = parsedAll; // parse all attribute
                    boolean reparse = false; // has Reparse annotation

                    Annotation[] anns = annotations[i];
                    Class class2 = classes[i];

                    if (anns == null) {
                        logger.trace("Failure(parameter without annotation): " + method.toGenericString());
                        find = false;
                        break;
                    }
                    String attributeName = null;
                    for (Annotation ann : anns) {
                        if (ann instanceof Attribute) {
                            attributeName = ((Attribute) ann).value();
                        }
                        if (ann instanceof Primal) {
                            parsed = false;
                        }
                        if (ann instanceof Reparse) {
                            reparse = true;
                        }
                    }

                    if (StringUtils.isBlank(attributeName)) {
                        logger.trace("Failure(annotation value is empty): " + method.toGenericString());
                        find = false;
                        break;
                    }
                    if (!attributes.containsKey(attributeName)) {
                        logger.trace("Failure(can't find attribute[" + attributeName + "]): "
                                + method.toGenericString());
                        find = false;
                        break;
                    }

                    Object attrValue = null;
                    try {
                        // parse the parameter of process method
                        if (parsed) {
                            if (reparse) {
                                attrValue = env.parse("{" + attributes.get(attributeName) + "}");
                            } else {
                                attrValue = env.parse(attributes.get(attributeName));
                            }
                        } else {
                            attrValue = attributes.get(attributeName);
                        }

                    } catch (UnsupportedExpressionException e) {
                        logger.trace("Failure(error[" + attributeName + ", " + e.getMessage() + "]): "
                                + method.toGenericString());
                        find = false;
                        //break;
                        throw new UnsupportedScriptException(this, e);
                    }
                    Class class1 = attrValue.getClass();

                    if (class2.isAssignableFrom(class1) || class2 == Object.class) {
                        objs[i] = attrValue;

                    } else if (class1 == String.class) {
                        try {
                            Object obj = Environment.convert((String) attrValue, class2);
                            if (obj == null) {
                                logger.trace("Failure(Convert failure[" + attributeName + ", null]): "
                                        + method.toGenericString());
                                find = false;
                                break;
                            } else {
                                objs[i] = obj;
                            }
                        } catch (Exception e) {
                            logger.trace("Failure(Convert exception[" + attributeName + "]): "
                                    + method.toGenericString());
                            find = false;
                            break;
                            //UnsupportedScriptException ex = new UnsupportedScriptException(this, e);
                            //throw ex;
                        }
                    } else {
                        logger.trace("Failure(Unsupported class[" + attributeName + ", " + class1 + ", " + class2
                                + "]): " + method.toGenericString());
                        find = false;
                        break;
                    }
                }
            }

            if (find) {
                try {
                    logger.debug("Invoke: " + method.toGenericString());
                    if (method.getReturnType() == int.class) {
                        return (Integer) method.invoke(this, objs);
                    } else if (method.getReturnType() == Integer.class) {
                        return (Integer) method.invoke(this, objs);
                    } else {
                        method.invoke(this, objs);
                        return NONE;
                    }
                } catch (InvocationTargetException e) {
                    Throwable t = e.getTargetException();
                    if (t instanceof UnsupportedScriptException) {
                        throw (UnsupportedScriptException) t;
                    } else {
                        UnsupportedScriptException ex = new UnsupportedScriptException(this, e);
                        throw ex;
                    }

                } catch (Exception e) {
                    UnsupportedScriptException ex = new UnsupportedScriptException(this, e);
                    throw ex;
                }
            }
        }

        throw new UnsupportedScriptException(this, "Can't fine the method to process.");
    }

    @Override
    public Element getParent() {
        return parent;
    }

    @Override
    public void setParent(Element e) {
        this.parent = e;
    }

    /**
     * Invoke all child elements
     * @return
     * @throws UnsupportedScriptException
     */
    protected int invokeChildren() throws UnsupportedScriptException {
        return invokeChildren(0);
    }

    private int invokeChildren(int i) throws UnsupportedScriptException {
        Vector<Element> children = this.getChildren();

        int r = NONE;
        for (; i < children.size(); i++) {
            Element child = children.get(i);
            r = child.invoke();
            if (r != NONE)
                break;
        }

        // If EXCEPTION is return, find Catch element to handle.
        if (r == EXCEPTION) {
            for (; i < children.size(); i++) {
                Element child = children.get(i);
                if (child instanceof kenh.xscript.elements.Catch) {
                    r = ((kenh.xscript.elements.Catch) child).processException();
                    break;
                }
            }
            if (r != NONE)
                return r;
            else {
                // If Catch element return NONE, go on with children after Catch element.
                invokeChildren(i);
            }
        }

        return r;
    }

    /**
     * Get the element information ( name and attribute ), this method is used to debug.
     * @return
     */
    protected String getInfo() {
        StringBuffer info = new StringBuffer();

        info.append("<" + this.getClass().getCanonicalName());

        // 
        Map<String, String> attributes = this.getAttributes();
        if (attributes != null && attributes.size() > 0) {
            Set<String> keys = attributes.keySet();
            for (String key : keys) {
                info.append(" " + key + "=\"" + attributes.get(key) + "\"");
            }
        }

        info.append(">");

        return info.toString();
    }

    public static final String MODI_FINAL = "final"; // modifier, like java key word 'final', mean constant.
    public static final String MODI_PUBLIC = "public"; // modifier, like java key word 'public'

    protected void saveVariable(String var, Object value, Object defaultValue) throws UnsupportedScriptException {

        boolean isFinal = false;
        boolean isPublic = false;
        if (this.getParent() instanceof Script)
            isPublic = true;

        String varName = var;
        if (StringUtils.contains(var, " ")) {
            String[] all = StringUtils.split(var, " ");
            varName = StringUtils.trimToEmpty(all[all.length - 1]);
            if (StringUtils.isBlank(varName)) {
                UnsupportedScriptException ex = new UnsupportedScriptException(this,
                        "Variable format incorrect. [" + var + "]");
                throw ex;
            }

            for (int i = 0; i < all.length - 1; i++) {
                String s = StringUtils.trimToEmpty(all[i]);
                if (StringUtils.isBlank(s))
                    continue;
                if (s.equals(MODI_FINAL)) {
                    isFinal = true;
                } else if (s.equals(MODI_PUBLIC)) {
                    isPublic = true;
                } else {
                    UnsupportedScriptException ex = new UnsupportedScriptException(this,
                            "Unsupported modifier. [" + var + ", " + s + "]");
                    throw ex;
                }
            }
        }

        // if value if empty, use default value.
        if (value == null || ((value instanceof String) && ((String) value).equals(""))) {
            if (defaultValue != null)
                value = defaultValue;
        }

        setVariable(varName, value, isFinal, isPublic);
    }

    /**
     * Store the variable
     * 
     * @param var
     * @param value
     * @param isFinal
     * @param isPublic
     * @throws UnsupportedScriptException
     */
    protected void setVariable(String var, Object value, boolean isFinal, boolean isPublic)
            throws UnsupportedScriptException {
        try {
            if (isPublic)
                this.getEnvironment().setPublicVariable(var, value, isFinal);
            else
                this.getEnvironment().setVariable(var, value, isFinal);
        } catch (Exception e) {
            throw new UnsupportedScriptException(this, e);
        }
    }

}