zk/src/main/java/org/zkoss/zk/ui/metainfo/impl/ComponentDefinitionImpl.java

Summary

Maintainability
D
2 days
Test Coverage
/* ComponentDefinitionImpl.java

    Purpose:
        
    Description:
        
    History:
        Tue May 31 17:54:45     2005, Created by tomyeh

Copyright (C) 2005 Potix Corporation. All Rights Reserved.

{{IS_RIGHT
    This program is distributed under LGPL Version 2.1 in the hope that
    it will be useful, but WITHOUT ANY WARRANTY.
}}IS_RIGHT
*/
package org.zkoss.zk.ui.metainfo.impl;

import java.net.URL;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.zkoss.lang.Classes;
import org.zkoss.util.resource.Location;
import org.zkoss.web.servlet.Servlets;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Page;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.metainfo.AnnotationMap;
import org.zkoss.zk.ui.metainfo.ComponentDefinition;
import org.zkoss.zk.ui.metainfo.ComponentInfo;
import org.zkoss.zk.ui.metainfo.LanguageDefinition;
import org.zkoss.zk.ui.metainfo.PageDefinition;
import org.zkoss.zk.ui.metainfo.Property;
import org.zkoss.zk.ui.metainfo.ShadowInfo;
import org.zkoss.zk.ui.sys.ComponentsCtrl;
import org.zkoss.zk.xel.EvaluatorRef;
import org.zkoss.zk.xel.ExValue;
import org.zkoss.zk.xel.impl.Utils;

/**
 * An implementation of {@link ComponentDefinition}.
 * 
 * <p>Note: it is not thread safe. Thus, it is better to {@link #clone}
 * and then modifying the cloned instance if you want to change it
 * concurrently.
 *
 * @author tomyeh
 */
public class ComponentDefinitionImpl implements ComponentDefinition, java.io.Serializable {
    private String _name;
    private transient LanguageDefinition _langdef;
    private transient PageDefinition _pgdef;
    private EvaluatorRef _evalr;
    /** Either String or Class. */
    private Object _implcls;
    /** A map of (String mold, ExValue widgetClass). */
    private Map<String, ExValue> _molds;
    /** The default widget class. */
    private ExValue _defWgtClass;
    /** A map of custom attributes (String name, ExValue value). */
    private Map<String, ExValue> _custAttrs;
    /** A list of {@link Property}. */
    private List<Property> _props;
    /** the current directory. */
    private String _curdir;
    /** the property name to which the text within the element will be assigned. */
    private String _textAs;
    private AnnotationMap _annots;
    private URL _declURL;
    /** The parsed expressions of the apply attribute. */
    private ExValue[] _apply;
    /** Whether to preserve the blank text. */
    private boolean _blankpresv;
    /** Whether the child element is allowed within the element. */
    private boolean _childAllowedInTextAs;

    /** Constructs a native component, i.e., a component implemented by
     * a Java class.
     *
     * <p>Note; if both langdef and pgdef are null, it must be a reserved
     * component.
     *
     * @param langdef the language definition. It is null if it is defined
     * as part of a page definition
     * @param pgdef the page definition. It is null if it is defined
     * as part of a language definition.
     * doesn't belong to any language.
     * @param cls the implementation class.
     * @since 3.0.0
     */
    public ComponentDefinitionImpl(LanguageDefinition langdef, PageDefinition pgdef, String name,
            Class<? extends Component> cls) {
        if (cls != null && !Component.class.isAssignableFrom(cls))
            throw new IllegalArgumentException(cls + " must implement " + Component.class);
        init(langdef, pgdef, name, cls);
    }

    /** Constructs a native component, i.e., a component implemented by
     * a Java class.
     *
     * <p>Note; if both langdef and pgdef are null, it must be a reserved
     * component.
     *
     * @param langdef the language definition. It is null if it is defined
     * as part of a page definition
     * @param pgdef the page definition. It is null if it is defined
     * as part of a language definition.
     * doesn't belong to any language.
     * @param clsnm the implementation class.
     * @since 3.0.8
     */
    public ComponentDefinitionImpl(LanguageDefinition langdef, PageDefinition pgdef, String name, String clsnm) {
        init(langdef, pgdef, name, clsnm);
    }

    private void init(LanguageDefinition langdef, PageDefinition pgdef, String name, Object cls) {
        if (name == null)
            throw new IllegalArgumentException();
        if (langdef != null && pgdef != null)
            throw new IllegalArgumentException("langdef and pgdef cannot both null or both non-null");

        _langdef = langdef;
        _pgdef = pgdef;
        _name = name;
        _implcls = cls;

        _evalr = _langdef != null ? _langdef.getEvaluatorRef() : _pgdef != null ? _pgdef.getEvaluatorRef() : null;
    }

    /** Constructs a shadow component definition.
     * It is the component definition used to implement the shadow element.
     *
     * @param langdef the language definition. It is null if it is defined
     * as part of a page definition
     * @param pgdef the page definition. It is null if it is defined
     * as part of a language definition.
     * @param templateURI the URI of the ZUML page to representing this shadow, like macroURI.
     * @since 8.0.0
     */
    public static final ComponentDefinition newShadowDefinition(LanguageDefinition langdef, PageDefinition pgdef,
            String name, Class<? extends Component> cls, String templateURI) {
        return new ShadowDefinitionImpl(langdef, pgdef, name, cls, templateURI);
    }

    /** Constructs a macro component definition.
     * It is the component definition used to implement the macros.
     *
     * @param langdef the language definition. It is null if it is defined
     * as part of a page definition
     * @param pgdef the page definition. It is null if it is defined
     * as part of a language definition.
     * @param macroURI the URI of the ZUML page to representing this macro.
     * @since 3.0.0
     */
    public static final ComponentDefinition newMacroDefinition(LanguageDefinition langdef, PageDefinition pgdef,
            String name, Class<? extends Component> cls, String macroURI, boolean inline) {
        return new MacroDefinition(langdef, pgdef, name, cls, macroURI, inline);
    }

    /** Constructs a native component definition.
     * It is the component definition used to implement the native namespace.
     *
     * @param langdef the language definition. It cannot be null.
     * @since 3.0.0
     */
    public static final ComponentDefinition newNativeDefinition(LanguageDefinition langdef, String name,
            Class<? extends Component> cls) {
        return new NativeDefinition(langdef, name, cls);
    }

    //extra//
    /** Adds a custom attribute.
     */
    public void addCustomAttribute(String name, String value) {
        if (name == null || value == null || name.length() == 0 || value.length() == 0)
            throw new IllegalArgumentException();

        final ExValue ev = new ExValue(value, Object.class);
        if (_custAttrs == null)
            _custAttrs = new HashMap<String, ExValue>(4);
        _custAttrs.put(name, ev);
    }

    /** Adds an annotation to the specified property of this component
     * definition.
     *
     * @param propName the property name.
     * If null, the annotation is associated with the whole component rather than
     * a particular property.
     * @param annotName the annotation name (never null, nor empty).
     * @param annotAttrs a map of attributes, or null if no attribute at all.
     * The attribute must be in a pair of strings (String name, String value).
     * @param loc the location information of the annotation in
     * the document, or null if not available.
     * @since 6.0.0
     */
    public void addAnnotation(String propName, String annotName, Map<String, String[]> annotAttrs, Location loc) {
        if (_annots == null)
            _annots = new AnnotationMap();
        _annots.addAnnotation(propName, annotName, annotAttrs, loc);
    }

    /** Returns the current directory which is used to convert
     * a relative URI to absolute, or null if not available.
     */
    public String getCurrentDirectory() {
        return _curdir;
    }

    /** Sets the current directory which is used to convert
     * a relative URI to absolute.
     *
     * @param curdir the current directory; null to ignore.
     */
    public void setCurrentDirectory(String curdir) {
        if (curdir != null && curdir.length() > 0) {
            _curdir = curdir.charAt(curdir.length() - 1) != '/' ? curdir + '/' : curdir;
        } else {
            _curdir = null;
        }
    }

    /** Sets the property name to which the text enclosed within
     * the element (associated with this component definition) is assigned to.
     *
     * <p>Default: null (means to create a Label component)
     *
     * @param propnm the property name. If empty (""), null is assumed.
     * @see #getTextAs
     * @since 3.0.0
     */
    public void setTextAs(String propnm) {
        _textAs = propnm != null && propnm.length() > 0 ? propnm : null;
    }

    /** Sets whether to preserve the blank text.
     * If false, the blank text (a non-empty string consisting of whitespaces)
     * are ignored.
     * If true, they are converted to a label child.
     * <p>Default: false.
     * @see #isBlankPreserved
     * @since 3.5.0
     */
    public void setBlankPreserved(boolean preserve) {
        _blankpresv = preserve;
    }

    /** Sets whether the child component is allowed within the element.
     * For more information, please refer to {@link ComponentDefinition#getTextAs}.
     * @since 6.0.0
     */
    public void setChildAllowedInTextAs(boolean allowed) {
        _childAllowedInTextAs = allowed;
    }

    /** Sets the URI where this definition is declared.
     *
     * @param url the URL. If null, it means not available.
     * @since 3.0.3
     */
    public void setDeclarationURL(URL url) {
        _declURL = url;
    }

    //ComponentDefinition//
    public LanguageDefinition getLanguageDefinition() {
        return _langdef;
    }

    public String getName() {
        return _name;
    }

    public String getTextAs() {
        return _textAs;
    }

    public boolean isChildAllowedInTextAs() {
        return _childAllowedInTextAs;
    }

    public boolean isBlankPreserved() {
        return _blankpresv;
    }

    public boolean isMacro() {
        return false;
    }

    public String getMacroURI() {
        return null;
    }

    public boolean isInlineMacro() {
        return false;
    }

    public boolean isNative() {
        return false;
    }

    public Object getImplementationClass() {
        return _implcls;
    }

    public void setImplementationClass(Class<? extends Component> cls) {
        if (!Component.class.isAssignableFrom(cls))
            throw new UiException(Component.class.getName() + " must be implemented by " + cls);
        _implcls = cls;
    }

    public void setImplementationClass(String clsnm) {
        if (clsnm == null || clsnm.length() == 0)
            throw new UiException("Non-empty class name is required");
        _implcls = clsnm;
    }

    @SuppressWarnings("unchecked")
    public Component newInstance(Page page, String clsnm) {
        try {
            return newInstance((Class<? extends Component>) resolveImplementationClass(page, clsnm));
        } catch (ClassNotFoundException ex) {
            throw UiException.Aide.wrap(ex);
        }
    }

    public Component newInstance(Class<? extends Component> cls) {
        final Object curInfo = ComponentsCtrl.getCurrentInfo();
        boolean bSet = true;
        if (curInfo instanceof ComponentInfo) {
            bSet = ((ComponentInfo) curInfo).getComponentDefinition() != this;
        } else if (curInfo instanceof ShadowInfo) {
            bSet = ((ShadowInfo) curInfo).getComponentDefinition() != this;
        }
        if (bSet)
            ComponentsCtrl.setCurrentInfo(this);
        final Component comp;
        try {
            comp = cls.newInstance();
        } catch (Exception ex) {
            throw UiException.Aide.wrap(ex);
        } finally {
            if (bSet)
                ComponentsCtrl.setCurrentInfo((ComponentDefinition) null);
        }
        return comp;
    }

    public boolean isInstance(Component comp) {
        Class<?> cls;
        if (_implcls instanceof String) {
            try {
                cls = resolveImplementationClass(comp.getPage(), null);
            } catch (ClassNotFoundException ex) {
                return true; //consider as true if not resolvable
            }
        } else {
            cls = (Class<?>) _implcls;
        }
        return cls.isInstance(comp);
    }

    public Class<?> resolveImplementationClass(Page page, String clsnm) throws ClassNotFoundException {
        final Object cls = clsnm != null ? clsnm : _implcls;
        if (cls instanceof String) {
            clsnm = (String) cls;
            final Class<?> found = page != null ? page.resolveClass(clsnm) : Classes.forNameByThread(clsnm);
            if (clsnm.equals(_implcls))
                _implcls = found;
            //cache to _implcls (to improve the performance)
            return found;
        }
        return (Class) cls;
    }

    public AnnotationMap getAnnotationMap() {
        return _annots;
    }

    public String getApply() {
        if (_apply == null)
            return null;

        final StringBuffer sb = new StringBuffer();
        for (int j = 0; j < _apply.length; ++j) {
            if (j > 0)
                sb.append(',');
            sb.append(_apply[j].getRawValue());
        }
        return sb.toString();
    }

    public void setApply(String apply) {
        _apply = Utils.parseList(apply, Object.class, true);
    }

    public ExValue[] getParsedApply() {
        return _apply;
    }

    public URL getDeclarationURL() {
        return _declURL;
    }

    public void addProperty(String name, String value) {
        //Implementation Note: the reason not to have condition because
        //isEffective always assumes Executions.getCurrent, which is
        //not true if _langdef != null

        if (name == null || name.length() == 0)
            throw new IllegalArgumentException("name");

        final Property prop = new Property(_evalr, name, value, null);
        if (_props == null)
            _props = new LinkedList<Property>();
        _props.add(prop);
    }

    public void applyProperties(Component comp) {
        //Note: it doesn't apply annotations since it is done
        //by AbstractComponent's constructor
        //AbstractComponent's constructor also invokes applyAttributes automatically

        if (_props != null) {
            for (Property prop : _props) {
                prop.assign(comp);
            }
        }
    }

    public void applyAttributes(Component comp) {
        if (_custAttrs != null) {
            for (Map.Entry<String, ExValue> me : _custAttrs.entrySet()) {
                comp.setAttribute(me.getKey(), me.getValue().getValue(_evalr, comp));
            }
        }
    }

    public Map<String, Object> evalProperties(Map<String, Object> propmap, Page owner, Component parent) {
        if (propmap == null)
            propmap = new HashMap<String, Object>();

        if (_props != null) {
            for (Property prop : _props) {
                if (parent != null) {
                    if (prop.isEffective(parent))
                        propmap.put(prop.getName(), prop.getValue(parent));
                } else {
                    if (prop.isEffective(owner))
                        propmap.put(prop.getName(), prop.getValue(owner));
                }
            }
        }
        return propmap;
    }

    public void addMold(String name, String widgetClass) {
        if (name == null || name.length() == 0)
            throw new IllegalArgumentException();

        if (_molds == null)
            _molds = new HashMap<String, ExValue>(2);
        _molds.put(name, new ExValue(widgetClass, String.class));
    }

    public boolean hasMold(String name) {
        return _molds != null && _molds.containsKey(name);
    }

    public Collection<String> getMoldNames() {
        if (_molds != null)
            return _molds.keySet();
        return Collections.emptyList();
    }

    public String getWidgetClass(Component comp, String moldName) {
        if (_molds != null) {
            final ExValue wc = _molds.get(moldName);
            if (wc != null) {
                final String s = (String) wc.getValue(_evalr, comp);
                if (s != null)
                    return s;
            }
        }
        return getDefaultWidgetClass(comp);
    }

    public String getDefaultWidgetClass(Component comp) {
        return _defWgtClass != null ? (String) _defWgtClass.getValue(_evalr, comp) : null;
    }

    public void setDefaultWidgetClass(String widgetClass) {
        final ExValue oldwc = _defWgtClass;
        _defWgtClass = new ExValue(widgetClass, String.class);

        //replace mold's widget class if it is the old default one
        if (oldwc != null && _molds != null)
            for (Map.Entry<String, ExValue> me : _molds.entrySet()) {
                if (oldwc.equals(me.getValue()))
                    me.setValue(_defWgtClass);
            }
    }

    private String toAbsoluteURI(String uri) {
        if (_curdir != null && uri != null && uri.length() > 0) {
            final char cc = uri.charAt(0);
            if (cc != '/' && cc != '~' && !Servlets.isUniversalURL(uri))
                return _curdir + uri;
        }
        return uri;
    }

    public boolean isShadowElement() {
        return false;
    }

    //Serializable//
    //NOTE: they must be declared as private
    private synchronized void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
        s.defaultWriteObject();

        s.writeObject(_langdef != null ? _langdef.getName() : null);
    }

    private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();

        final String langnm = (String) s.readObject();
        if (langnm != null)
            _langdef = LanguageDefinition.lookup(langnm);
    }

    //Object//
    public String toString() {
        return "[ComponentDefinition: " + _name + ']';
    }

    //Cloneable/
    public Object clone() {
        final ComponentDefinitionImpl compdef;
        try {
            compdef = (ComponentDefinitionImpl) super.clone();
        } catch (CloneNotSupportedException ex) {
            throw new InternalError();
        }

        if (_annots != null)
            compdef._annots = (AnnotationMap) _annots.clone();
        if (_props != null)
            compdef._props = new LinkedList<Property>(_props);
        if (_molds != null)
            compdef._molds = new HashMap<String, ExValue>(_molds);
        if (_custAttrs != null)
            compdef._custAttrs = new HashMap<String, ExValue>(_custAttrs);
        return compdef;
    }

    public ComponentDefinition clone(LanguageDefinition langdef, String name) {
        if (name == null || name.length() == 0)
            throw new IllegalArgumentException("empty");

        ComponentDefinitionImpl cd = (ComponentDefinitionImpl) clone();
        cd._name = name;
        cd._langdef = langdef;
        return cd;
    }
}