All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.nuiton.jaxx.compiler.CompiledObject Maven / Gradle / Ivy

/*
 * #%L
 * JAXX :: Compiler
 * %%
 * Copyright (C) 2008 - 2018 Code Lutin, Ultreia.io
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program 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 General Lesser Public License for more details.
 *
 * You should have received a copy of the GNU General Lesser Public
 * License along with this program.  If not, see
 * .
 * #L%
 */

package org.nuiton.jaxx.compiler;

import org.apache.commons.lang3.StringUtils;
import org.nuiton.jaxx.compiler.reflect.ClassDescriptor;
import org.nuiton.jaxx.compiler.reflect.ClassDescriptorHelper;
import org.nuiton.jaxx.compiler.reflect.MethodDescriptor;
import org.nuiton.jaxx.compiler.tags.DefaultComponentHandler;
import org.nuiton.jaxx.compiler.tags.TagHandler;
import org.nuiton.jaxx.compiler.tags.TagManager;
import org.nuiton.jaxx.compiler.types.TypeManager;
import org.nuiton.jaxx.runtime.JAXXUtil;

import java.awt.Container;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

/**
 * Represents an object in the .java file being generated during compilation.  There is
 * a CompiledObject for each class tag encountered, and certain tags may generate
 * additional objects for various reasons.
 */
public class CompiledObject {

    /** The object's id. */
    private final String id;

    /** Java code referring to the object. */
    private final String javaCode;

    /** The object's class. */
    private ClassDescriptor objectClass;

    /** The style class. */
    private String styleClass;
    private Set styleClasses;

    /** The container containing this CompiledObject. */
    private CompiledObject parent;

    /**
     * true if this object overrides an object of the same id in a superclass
     * of the object being compiled
     */
    private boolean override;

    /**
     * The type of the object without fqn if possible.
     *
     * This is used when casting on an overridden object and only available if
     * {@link #override} is set to {@code true}.
     */
    private String simpleType;

    /**
     * Comma-separated Java code snippets representing the parameters that
     * should be passed to the object's constructor.
     */
    private String constructorParams;

    /**
     * Java code snippet which performs basic initialization of the object (after it has already been constructed).
     * Because CompiledObject initialization order cannot be guaranteed, it is not safe to refer to other
     * CompiledObjects from initializationCode -- you must refer to them from additionCode instead.
     */
    private final StringBuilder initializationCode = new StringBuilder();

    /**
     * Java code snippet which completes setup by adding any child objects, or otherwise manipulates any refererenced
     * objects.  Because CompiledObject initialization order cannot be guaranteed, it is not safe to refer to other
     * CompiledObjects from initializationCode -- you must refer to them from additionCode instead.
     */
    private StringBuilder additionCode = new StringBuilder();

    /** List of all registered event handlers. */
    private final List eventHandlers = new ArrayList<>();

    /** All properties that have been applied to this CompiledObject. */
    private final Map properties = new HashMap<>();

    /** generic types of the compiled object */
    private String[] genericTypes;

    /**
     * a flag to indicate if javaBean full support must be support for this
     * object by root object
     */
    private boolean javaBean;

    /** code to initialize the bean (can be null) */
    private String javaBeanInitCode;

    /** the type of the override object (can be null if no overide) */
    private ClassDescriptor overrideType;

    /** the decorator */
    private CompiledObjectDecorator decorator;

    /** client properties */
    private Map clientProperties;

    /** initializer of the object */
    private String initializer;

    private String i18nProperty;
    private BeanScope beanScope;

    /** Should we force to add the object to parent container when overrides **/
    private boolean addToContainer;

    public String getSimpleType() {
        return simpleType;
    }

    public Optional getBeanScope() {
        return Optional.ofNullable(beanScope);
    }

    public void setBeanScope(BeanScope beanScope) {
        this.beanScope = Objects.requireNonNull(beanScope);
    }

    public void setAddToContainer(boolean addToContainer) {
        this.addToContainer = addToContainer;
    }

    public boolean isAddToContainer() {
        return addToContainer;
    }

    public class ChildRef {

        final CompiledObject child;

        final String constraints;

        String childJavaCode;

        private final String delegateCode;

        public ChildRef(CompiledObject child,
                        String constraints,
                        String childJavaCode,
                        String delegateCode) {
            this.child = child;
            this.constraints = constraints;
            this.childJavaCode = childJavaCode;
            this.delegateCode = delegateCode;
        }


        public String getConstraints() {
            return constraints;
        }

        public String getDelegateCode() {
            return delegateCode;
        }

        public CompiledObject getChild() {
            return child;
        }

        public String getChildJavaCode() {
            return childJavaCode;
        }

        public void setChildJavaCode(String childJavaCode) {
            this.childJavaCode = childJavaCode;
        }

        public void addToAdditionCode(StringBuilder buffer,
                                      boolean isRootObject) {
            //TC-20091026 do not prefix if on root object
            String prefix;
            if (isRootObject) {
                prefix = "";
            } else {
                prefix = javaCode + delegateCode + ".";
            }
            if (constraints != null) {
                buffer.append(prefix);
                buffer.append("add(");
                buffer.append(childJavaCode);
                buffer.append(", ");
                buffer.append(constraints);
                buffer.append(");");
            } else {
                buffer.append(prefix);
                buffer.append("add(");
                buffer.append(childJavaCode);
                buffer.append(");");
            }
            buffer.append(JAXXCompiler.getLineSeparator());
        }
    }

    private List childs;

    /**
     * Creates a new CompiledObject.
     *
     * To be useful, the object should be registered with a
     * JAXXCompiler using
     * {@link JAXXCompiler#registerCompiledObject(CompiledObject)} .
     *
     * @param id          the object's id
     * @param objectClass the object's class
     * @param compiler    the current JAXXCompiler
     * @throws NullPointerException if id or class is null
     */
    public CompiledObject(String id,
                          ClassDescriptor objectClass,
                          JAXXCompiler compiler) {
        this(id, objectClass, compiler, false);
    }

    /**
     * Creates a new CompiledObject.  To be useful, the object should be registered with a
     * JAXXCompiler using {@link JAXXCompiler#registerCompiledObject(CompiledObject)} .
     *
     * @param id          the object's id
     * @param objectClass the object's class
     * @param compiler    the current JAXXCompiler
     * @param force       true to force acceptance of invalid ids
     * @throws NullPointerException if id or class is null
     */
    public CompiledObject(String id,
                          ClassDescriptor objectClass,
                          JAXXCompiler compiler,
                          boolean force) {
        this(id, id, objectClass, compiler, force);
    }

    /**
     * Creates a new CompiledObject.  To be useful, the object should be registered with a
     * JAXXCompiler using {@link JAXXCompiler#registerCompiledObject(CompiledObject)} .
     *
     * @param id          the object's id
     * @param javaCode    Java code referring to the object
     * @param objectClass the object's class
     * @param force       true to force acceptance of invalid ids
     * @param compiler    the current JAXXCompiler
     * @throws CompilerException    if the id is not a valid Java identifier
     * @throws NullPointerException if id or class is null
     */
    public CompiledObject(String id,
                          String javaCode,
                          ClassDescriptor objectClass,
                          JAXXCompiler compiler,
                          boolean force) throws CompilerException {
        if (!force) {
            if (!isValidID(id)) {
                compiler.reportError(
                        "the id '" + id + "' is not a valid Java identifier");
            }
        }
        this.id = id;
        this.javaCode = javaCode;

        if (objectClass == null) {
            throw new NullPointerException();
        }
        this.objectClass = objectClass;
        childs = new ArrayList<>();
    }

    public static boolean isValidID(String id) {
        boolean valid = true;
        if (id.length() == 0) {
            valid = false;
        }
        if (valid) {
            if (!Character.isJavaIdentifierStart(id.charAt(0))) {
                valid = false;
            }
            if (valid) {
                for (int i = 1; i < id.length(); i++) {
                    if (!Character.isJavaIdentifierPart(id.charAt(i))) {
                        valid = false;
                        break;
                    }
                }
            }
        }
        return valid;
    }

    /**
     * True if this object overrides an object in the superclass of the class
     * being compiled.  For this to be true, the class currently being compiled
     * must be a subclass of another JAXXObject which has an
     * identically-named object.
     *
     * @return true if this object is an override
     * @see #setOverride(boolean)
     */
    public boolean isOverride() {
        return override;
    }

    /**
     * {@code true} when overrides an object in the superclass of the class
     * being compiled AND type is also override.
     *
     * @return {@code true} if this object is an override AND override type
     */
    public boolean isOverrideType() {
        return isOverride() && !getObjectClass().equals(getOverrideType());
    }

    /**
     * Sets whether this class overrides an identically-named object in the
     * parent class.
     *
     * @param override true if this object is an override
     * @see #isOverride
     */
    public void setOverride(boolean override) {
        this.override = override;
    }

    /**
     * Sets the simple type of the object.
     *
     * Used when castin an overridden object.
     *
     * @param simpleType the simple type to use (or the fqn if there is a
     *                   conflict with already imported types of the compiler).
     * @since 2.4
     */
    public void setSimpleType(String simpleType) {
        this.simpleType = simpleType;
    }

    /**
     * Returns this object's CSS style class.
     *
     * @return the value of the styleClass attribute
     */
    public String getStyleClass() {
        return styleClass;
    }

    public Set getStyleClasses() {
        return styleClasses== null
                ?styleClasses= styleClass==null? Collections.emptySet(): new LinkedHashSet<>(Arrays.asList(styleClass.trim().split("\\s"))) :
                styleClasses;
    }

    public boolean matchStyleClass(String styleClass) {
        return styleClass==null || getStyleClasses().contains(styleClass);
    }

    /**
     * Sets this object's CSS style class.
     *
     * @param styleClass the new style class
     */
    public void setStyleClass(String styleClass) {
        this.styleClass = styleClass;
        styleClasses = null;
    }

    /**
     * Returns this object's parent container.  Non-visual components (and
     * the root container) return null.
     *
     * @return the object's parent container
     */
    public CompiledObject getParent() {
        return parent;
    }

    /**
     * Sets this object's parent container.
     *
     * @param parent the parent container
     * @throws IllegalArgumentException if parent is not a {@link Container}
     */
    public void setParent(CompiledObject parent) throws IllegalArgumentException {
        if (!ClassDescriptorHelper.getClassDescriptor(Container.class).isAssignableFrom(parent.getObjectClass())) {
            throw new IllegalArgumentException("parent must descend from java.awt.Container");
        }
        this.parent = parent;
    }

    /**
     * Returns the name of the method that should be generated in the compiled
     * .java file
     * in order to create this object.  This is just a suggestion and may be
     * ignored.
     *
     * @return the suggested name of the method which initializes this object
     */
    public String getCreationMethodName() {
        return "create" + StringUtils.capitalize(getId());
    }

    /**
     * Returns the name of the method that should be generated in the compiled
     * .java file in order to add children to this object.  This
     * is just a suggestion and may be ignored.
     *
     * @return the suggested name of the method which completes this object's setup
     */
    public String getAdditionMethodName() {
        return "addChildrenTo" + StringUtils.capitalize(getId());
    }

    /**
     * Returns the type of this object.
     *
     * @return the class this CompiledObject represents
     */
    public ClassDescriptor getObjectClass() {
        return objectClass;
    }

    /**
     * Returns this object's id.  Generally, a field with this name will be
     * created in the compiled .java
     * file in order to represent this object.
     *
     * @return the id used to refer to this object
     */
    public String getId() {
        return id;
    }

    /**
     * Returns Java code used to refer to this object in the compiled Java file.
     * This is usually the same as its id.
     *
     * @return the Java code for this object
     */
    public String getJavaCode() {
        String result = javaCode;
        if (isOverride()) {
            // handle cases where object is overridden to be a different class
            if (simpleType == null) {

                // means overridden but stil on the same type
                result = javaCode;
            } else {
                result = "((" + simpleType + ") " + javaCode + ")";
//            result = "((" + JAXXCompiler.getCanonicalName(this) + ") " + javaCode + ")";
            }
        }
        return result;
    }

    public String getJavaCodeForProperty(String property) {
        if (!isOverride() || simpleType == null) {
            return javaCode;
        }
        String result = "((" + simpleType + ") " + javaCode + ")";
//        String result = "((" + JAXXCompiler.getCanonicalName(this) + ") " + javaCode + ")";
//        String result = "((" + JAXXCompiler.getCanonicalName(getObjectClass()) + ") " + javaCode + ")";

        String methodName = StringUtils.capitalize(property);
        try {
            MethodDescriptor methodDescriptor = overrideType.getMethodDescriptor("get" + methodName);
            if (methodDescriptor != null) {
                if (overrideType.getMethodDescriptor("set" + methodName, methodDescriptor.getReturnType()) != null) {
                    // handle cases where object is overridden to be a different class
                    result = javaCode;
                }
            }
        } catch (NoSuchMethodException e) {
            // lazy error, do nothing
        }
        return result;
    }

    /**
     * Returns a list of comma-separated Java code snippets that represent the
     * parameters to pass to this object's constructor.
     *
     * @return the raw constructor params
     * @see #setConstructorParams(String)
     */
    public String getConstructorParams() {
        return constructorParams;
    }

    /**
     * Sets the parameters to pass to this object's constructor.
     *
     * @param constructorParams comma-separated Java code snippets representing
     *                          constructor params
     * @see #getConstructorParams
     */
    public void setConstructorParams(String constructorParams) {
        this.constructorParams = constructorParams;
    }

    public String getInitializer() {
        return initializer;
    }

    public void setInitializer(String initializer) {
        this.initializer = initializer;
    }

    /**
     * Returns the code that performs basic initialization of this object,
     * after it has already been constructed.
     * This basic code should not reference any other
     * CompiledObjects as they may not have been created yet.
     *
     * @param compiler compiler to use
     * @return the code which initializes this object
     */
    public String getInitializationCode(JAXXCompiler compiler) {
        StringBuilder result = new StringBuilder(initializationCode.toString());
        for (Object eventHandler : eventHandlers) {
            EventHandler handler = (EventHandler) eventHandler;
            result.append(getInitializationCode(handler, compiler));
        }
        return result.toString();
    }

    protected String getInitializationCode(EventHandler handler, JAXXCompiler compiler) {
        MethodDescriptor addMethod = handler.getAddMethod();
        ClassDescriptor listenerClass = addMethod.getParameterTypes()[0];
        String type = compiler.getImportedType(listenerClass.getName());
        String prefix = compiler.getImportedType(JAXXUtil.class);

        //TC-20091026 use 'this' instead of root object javaCode
        //TC-20091105 JAXXUtil.getEventListener is generic, no more need cast and use simple name
        return getJavaCode() + '.' + addMethod.getName() + "(" + prefix + ".getEventListener(" + type + ".class, " +
                TypeManager.getJavaCode(handler.getListenerMethod().getName()) + ", this, " +
                TypeManager.getJavaCode(compiler.getEventHandlerMethodName(handler)) + "));" + JAXXCompiler.getLineSeparator();
    }

    /**
     * Returns Java code to complete final setup on this object.  This code may
     * reference other CompiledObjects, as they are guaranteed to
     * have all been created by this point.
     *
     * @return code which adds children and performs final setup
     */
    public String getAdditionCode() {
        return additionCode.toString();
    }

    /**
     * Appends code to the initialization code block.  A line separator is
     * automatically appended to the end.
     *
     * @param code the code to add to the initialization block
     * @see #getInitializationCode(JAXXCompiler)
     */
    public void appendInitializationCode(String code) {
        if (!code.isEmpty()) {
            initializationCode.append(code);
            initializationCode.append(JAXXCompiler.getLineSeparator());
        }
    }

    /**
     * Appends code to the addition code block.  A line separator is
     * automatically appended to the end.
     *
     * @param code the code to add to the addition block
     * @see #getAdditionCode
     */
    public void appendAdditionCode(String code) {
        if (!code.isEmpty()) {
            additionCode.append(code);
            additionCode.append(JAXXCompiler.getLineSeparator());
        }
    }

    /**
     * Stores a property for this object.  The only effect of calling this
     * method is that the property will be returned by {@code getProperties()}.
     *
     * @param property the name of the property
     * @param value    the property's value
     * @see #getProperties()
     */
    public void addProperty(String property, String value) {
        properties.put(property, value);
    }

    public boolean hasClientProperties() {
        return clientProperties != null && !clientProperties.isEmpty();
    }

    public void addClientProperty(String property, String value) {
        getClientProperties().put(property, value);
    }

    public String getClientProperty(String key) {
        if (!hasClientProperties()) {
            return null;
        }
        return clientProperties.get(key);
    }

    public Map getClientProperties() {
        if (clientProperties == null) {
            clientProperties = new HashMap<>();
        }
        return clientProperties;
    }

    /**
     * Returns all properties which have been set for this object.
     *
     * @return a Map containing all properties defined for this object
     * @see #addProperty(String, String)
     */
    public Map/**/ getProperties() {
        return properties;
    }

    /**
     * Adds an event listener to this object.  The generated code will appear
     * in the initialization block.
     *
     * @param eventId        unique (per CompiledObject) identifier for the event handler
     * @param addMethod      the method which adds the event listener
     * @param listenerMethod the method (in the listener class) which is called when the event is fired
     * @param code           the Java code for the listenerMethod's body
     * @param compiler       the current JAXXCompiler
     * @see #getInitializationCode(EventHandler, JAXXCompiler)
     */
    public void addEventHandler(String eventId,
                                MethodDescriptor addMethod,
                                MethodDescriptor listenerMethod,
                                String code,
                                JAXXCompiler compiler) {
        EventHandler handler = new EventHandler(
                getId() + "." + eventId,
                getJavaCode(),
                addMethod,
                addMethod.getParameterTypes()[0],
                listenerMethod,
                code
        );
        compiler.registerEventHandler(handler);
        eventHandlers.add(handler);

        if (getJavaCode().contains(".")) {
            // object lives in another JAXX file and consequently its initialization code won't be output
            compiler.appendInitializerCode(
                    getInitializationCode(handler, compiler));
        }
    }

    /**
     * Adds a child component to this container.  The child is added without
     * layout constraints.
     *
     * @param child    the component to add
     * @param compiler the current JAXXCompiler
     * @throws CompilerException if this object is not a container
     * @see #addChild(CompiledObject, String, JAXXCompiler)
     */
    public void addChild(CompiledObject child,
                         JAXXCompiler compiler) throws CompilerException {
        addChild(child, null, compiler);
    }

    /**
     * Adds a child component to this container.  This variant allows the Java
     * code for a layout constraints object to be specified.
     *
     * @param child       the component to add
     * @param constraints Java code for the layout constraints object
     * @param compiler    the current JAXXCompiler
     * @throws CompilerException if this object is not a container
     * @see #addChild(CompiledObject, JAXXCompiler)
     */
    public void addChild(CompiledObject child,
                         String constraints,
                         JAXXCompiler compiler) throws CompilerException {
        try {
            if (constraints != null) {
                constraints = compiler.checkJavaCode(constraints);
            }
        } catch (CompilerException e) {
            compiler.reportError(
                    "While parsing 'constraints' attribute: " + e.getMessage());
        }

        if (!child.isOverride() || child.isAddToContainer()) {
            TagHandler tagHandler = TagManager.getTagHandler(getObjectClass());
            if (tagHandler instanceof DefaultComponentHandler &&
                    !((DefaultComponentHandler) tagHandler).isContainer()) {
                compiler.reportError("component " + this + " may not have children");
            }

            String containerDelegate = ((DefaultComponentHandler) tagHandler).getContainerDelegate();
            String delegateCode = containerDelegate != null ?
                    "." + containerDelegate + "()" :
                    "";

            child.setParent(this);

            ChildRef ref = newChildRef(child, constraints, delegateCode);
            childs.add(ref);
        }
    }

    protected ChildRef newChildRef(CompiledObject child, String constraints, String delegateCode) {
        return new ChildRef(child,
                            constraints,
                            child.getJavaCode(),
                            delegateCode
        );
    }

    @Override
    public String toString() {
        return getObjectClass().getName() + "[id='" + id + "']";
    }

    public String getGenericTypes() {
        if (getGenericTypesLength() == 0) {
            // not using it
            return "";
        }
        String result = "";
        for (int i = 0, j = getGenericTypesLength(); i < j; i++) {
            result += ", " + genericTypes[i];
        }
        return "< " + result.substring(2) + " >";
    }

    public void setGenericTypes(String... genericTypes) {
        if (genericTypes == null) {
            this.genericTypes = null;
            return;
        }
        this.genericTypes = new String[genericTypes.length];
        for (int i = 0, j = genericTypes.length; i < j; i++) {
            this.genericTypes[i] = genericTypes[i].trim();
        }
    }

    public boolean isJavaBean() {
        return javaBean;
    }

    public void setJavaBean(boolean javaBean) {
        this.javaBean = javaBean;
    }

    public ClassDescriptor getOverrideType() {
        return overrideType;
    }

    public void setOverrideType(ClassDescriptor overrideType) {
        this.overrideType = overrideType;
    }

    public String getJavaBeanInitCode() {
        return javaBeanInitCode;
    }

    public void setJavaBeanInitCode(String javaBeanInitCode) {
        this.javaBeanInitCode = javaBeanInitCode;
    }

    public List getChilds() {
        return childs;
    }

    public CompiledObjectDecorator getDecorator() {
        return decorator;
    }

    public void setDecorator(CompiledObjectDecorator decorator) {
        this.decorator = decorator;
    }

    public void finalizeCompiler(JAXXCompiler compiler) {

        List refList = getChilds();
        if (refList != null && !refList.isEmpty()) {
            // compute additionCode for all childs
            StringBuilder buffer = new StringBuilder();
            for (ChildRef childRef : refList) {
                childRef.addToAdditionCode(buffer,
                                           equals(compiler.getRootObject()));
            }
            additionCode = buffer.append(additionCode);
        }
    }

    public int getGenericTypesLength() {
        return genericTypes == null ? 0 : genericTypes.length;
    }

    public String getGetterName() {
        return "get" + StringUtils.capitalize(id);
    }

    public String getSetterName() {
        return "set" + StringUtils.capitalize(id);
    }

    public boolean isUseComputeI18n() {
        return i18nProperty!=null && !"skip".equals(i18nProperty);
    }

    public String getI18nProperty() {
        return i18nProperty;
    }

    public void setI18nProperty(String i18nProperty) {
        this.i18nProperty = i18nProperty;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy