zhtml/src/main/java/org/zkoss/zhtml/impl/AbstractTag.java

Summary

Maintainability
F
3 days
Test Coverage
/* AbstractTag.java

    Purpose:
        
    Description:
        
    History:
        Tue Oct  4 09:15:59     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.zhtml.impl;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

import org.zkoss.html.HTMLs;
import org.zkoss.image.Images;
import org.zkoss.lang.Objects;
import org.zkoss.xml.XMLs;
import org.zkoss.zk.au.DeferredValue;
import org.zkoss.zk.ui.AbstractComponent;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Desktop;
import org.zkoss.zk.ui.Execution;
import org.zkoss.zk.ui.Executions;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.WrongValueException;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zk.ui.ext.DynamicPropertied;
import org.zkoss.zk.ui.ext.RawId;
import org.zkoss.zk.ui.ext.render.DirectContent;
import org.zkoss.zk.ui.sys.BooleanPropertyAccess;
import org.zkoss.zk.ui.sys.ComponentCtrl;
import org.zkoss.zk.ui.sys.ComponentsCtrl;
import org.zkoss.zk.ui.sys.HtmlPageRenders;
import org.zkoss.zk.ui.sys.PropertyAccess;
import org.zkoss.zk.ui.sys.StringPropertyAccess;

/**
 * The raw component used to generate raw HTML elements.
 *
 * <p>
 * Note: ZHTML components ignore the page listener since it handles non-deferrable event listeners
 * (see {@link org.zkoss.zk.ui.event.Deferrable}).
 *
 * @author tomyeh
 */
public class AbstractTag extends AbstractComponent implements DynamicPropertied, RawId {
    static {
        addClientEvent(AbstractTag.class, Events.ON_CLICK, 0);
    }

    /** The tag name. */
    protected String _tagnm;
    private Map<String, Object> _props;

    protected AbstractTag(String tagname) {
        if (tagname == null || tagname.length() == 0)
            throw new IllegalArgumentException("A tag name is required");
        _tagnm = tagname;
    }

    protected AbstractTag() {
    }

    /**
     * Returns the CSS class. Due to Java's limitation, we cannot use the name called getClas.
     * <p>
     * Default: null (the default value depends on element).
     */
    public String getSclass() {
        return (String) getDynamicProperty("class");
    }

    /**
     * Sets the CSS class.
     */
    public void setSclass(String sclass) {
        setDynamicProperty("class", sclass);
    }

    /**
     * Returns the CSS style.
     * <p>
     * Default: null.
     */
    public String getStyle() {
        return (String) getDynamicProperty("style");
    }

    /**
     * Sets the CSS style.
     *
     * <p>
     * Note: if display is not specified as part of style, the returned value of {@link #isVisible}
     * is assumed. In other words, if not visible and display is not specified as part of style,
     * "display:none" is appended.
     *
     * <p>
     * On the other hand, if display is specified, then {@link #setVisible} is called to reflect the
     * visibility, if necessary.
     */
    public void setStyle(String style) {
        setDynamicProperty("style", style);
    }

    /**
     * Returns the accesskey of this tag.
     * <p>Notice that this attribute refers to the corresponding attribute of the HTML5 specification.
     * Hence, it would still be rendered to client-side as a DOM attribute even if the browser doesn’t support it.
     * @since 8.0.3
     */
    public String getAccesskey() {
        return (String) getDynamicProperty("accesskey");
    }

    /**
     * Sets the accesskey of this tag.
     * <p>Notice that this attribute refers to the corresponding attribute of the HTML5 specification.
     * Hence, it would still be rendered to client-side as a DOM attribute even if the browser doesn’t support it.
     * @since 8.0.3
     */
    public void setAccesskey(String accesskey) throws WrongValueException {
        setDynamicProperty("accesskey", accesskey);
    }
    /**
     * Returns the contenteditable of this tag.
     * <p>Notice that this attribute refers to the corresponding attribute of the HTML5 specification.
     * Hence, it would still be rendered to client-side as a DOM attribute even if the browser doesn’t support it.
     * @since 8.0.3
     */
    public boolean isContenteditable() {
        final Boolean b = (Boolean) getDynamicProperty("contenteditable");
        return b != null && b.booleanValue();
    }

    /**
     * Sets the contenteditable of this tag.
     * <p>Notice that this attribute refers to the corresponding attribute of the HTML5 specification.
     * Hence, it would still be rendered to client-side as a DOM attribute even if the browser doesn’t support it.
     * @since 8.0.3
     */
    public void setContenteditable(boolean contenteditable) throws WrongValueException {
        setDynamicProperty("contenteditable", contenteditable ? Boolean.valueOf(contenteditable) : null);
    }
    /**
     * Returns the dir of this tag.
     * <p>Notice that this attribute refers to the corresponding attribute of the HTML5 specification.
     * Hence, it would still be rendered to client-side as a DOM attribute even if the browser doesn’t support it.
     * @since 8.0.3
     */
    public String getDir() {
        return (String) getDynamicProperty("dir");
    }

    /**
     * Sets the dir of this tag.
     * <p>Notice that this attribute refers to the corresponding attribute of the HTML5 specification.
     * Hence, it would still be rendered to client-side as a DOM attribute even if the browser doesn’t support it.
     * @since 8.0.3
     */
    public void setDir(String dir) throws WrongValueException {
        setDynamicProperty("dir", dir);
    }
    /**
     * Returns the draggable of this tag.
     * <p>Notice that this attribute refers to the corresponding attribute of the HTML5 specification.
     * Hence, it would still be rendered to client-side as a DOM attribute even if the browser doesn’t support it.
     * @since 8.0.3
     */
    public boolean isDraggable() {
        final Boolean b = (Boolean) getDynamicProperty("draggable");
        return b != null && b.booleanValue();
    }

    /**
     * Sets the draggable of this tag.
     * <p>Notice that this attribute refers to the corresponding attribute of the HTML5 specification.
     * Hence, it would still be rendered to client-side as a DOM attribute even if the browser doesn’t support it.
     * @since 8.0.3
     */
    public void setDraggable(boolean draggable) throws WrongValueException {
        setDynamicProperty("draggable", draggable ? Boolean.valueOf(draggable) : null);
    }
    /**
     * Returns the hidden of this tag.
     * <p>Notice that this attribute refers to the corresponding attribute of the HTML5 specification.
     * Hence, it would still be rendered to client-side as a DOM attribute even if the browser doesn’t support it.
     * @since 8.0.3
     */
    public boolean isHidden() {
        final Boolean b = (Boolean) getDynamicProperty("hidden");
        return b != null && b.booleanValue();
    }

    /**
     * Sets the hidden of this tag.
     * <p>Notice that this attribute refers to the corresponding attribute of the HTML5 specification.
     * Hence, it would still be rendered to client-side as a DOM attribute even if the browser doesn’t support it.
     * @since 8.0.3
     */
    public void setHidden(boolean hidden) throws WrongValueException {
        setDynamicProperty("hidden", hidden ? Boolean.valueOf(hidden) : null);
    }
    /**
     * Returns the lang of this tag.
     * <p>Notice that this attribute refers to the corresponding attribute of the HTML5 specification.
     * Hence, it would still be rendered to client-side as a DOM attribute even if the browser doesn’t support it.
     * @since 8.0.3
     */
    public String getLang() {
        return (String) getDynamicProperty("lang");
    }

    /**
     * Sets the lang of this tag.
     * <p>Notice that this attribute refers to the corresponding attribute of the HTML5 specification.
     * Hence, it would still be rendered to client-side as a DOM attribute even if the browser doesn’t support it.
     * @since 8.0.3
     */
    public void setLang(String lang) throws WrongValueException {
        setDynamicProperty("lang", lang);
    }
    /**
     * Returns the spellcheck of this tag.
     * <p>Notice that this attribute refers to the corresponding attribute of the HTML5 specification.
     * Hence, it would still be rendered to client-side as a DOM attribute even if the browser doesn’t support it.
     * @since 8.0.3
     */
    public boolean isSpellcheck() {
        final Boolean b = (Boolean) getDynamicProperty("spellcheck");
        return b != null && b.booleanValue();
    }

    /**
     * Sets the spellcheck of this tag.
     * <p>Notice that this attribute refers to the corresponding attribute of the HTML5 specification.
     * Hence, it would still be rendered to client-side as a DOM attribute even if the browser doesn’t support it.
     * @since 8.0.3
     */
    public void setSpellcheck(boolean spellcheck) throws WrongValueException {
        setDynamicProperty("spellcheck", spellcheck ? Boolean.valueOf(spellcheck) : null);
    }
    /**
     * Returns the tabindex of this tag.
     * <p>Notice that this attribute refers to the corresponding attribute of the HTML5 specification.
     * Hence, it would still be rendered to client-side as a DOM attribute even if the browser doesn’t support it.
     * @since 8.0.3
     */
    public Integer getTabindex() {
        return (Integer) getDynamicProperty("tabindex");
    }

    /**
     * Sets the tabindex of this tag.
     * <p>Notice that this attribute refers to the corresponding attribute of the HTML5 specification.
     * Hence, it would still be rendered to client-side as a DOM attribute even if the browser doesn’t support it.
     * @since 8.0.3
     */
    public void setTabindex(Integer tabindex) throws WrongValueException {
        setDynamicProperty("tabindex", tabindex);
    }
    /**
     * Returns the title of this tag.
     * <p>Notice that this attribute refers to the corresponding attribute of the HTML5 specification.
     * Hence, it would still be rendered to client-side as a DOM attribute even if the browser doesn’t support it.
     * @since 8.0.3
     */
    public String getTitle() {
        return (String) getDynamicProperty("title");
    }

    /**
     * Sets the title of this tag.
     * <p>Notice that this attribute refers to the corresponding attribute of the HTML5 specification.
     * Hence, it would still be rendered to client-side as a DOM attribute even if the browser doesn’t support it.
     * @since 8.0.3
     */
    public void setTitle(String title) throws WrongValueException {
        setDynamicProperty("title", title);
    }

    /**
     * Returns the tag name.
     */
    public String getTag() {
        return _tagnm;
    }

    // -- DynamicPropertys --//
    public boolean hasDynamicProperty(String name) {
        return ComponentsCtrl.isReservedAttribute(name);
    }

    public Map<String, Object> getDynamicProperties() {
        return _props;
    }

    /**
     * Returns the dynamic property, or null if not found. Note: it must be a String object or null.
     */
    public Object getDynamicProperty(String name) {
        return _props != null ? _props.get(name) : null;
    }

    /**
     * Sets the dynamic property. Note: it converts the value to a string object (by use of
     * {@link Objects#toString}).
     *
     * <p>
     * Note: it handles the style property specially. Refer to {@link #setStyle} for details.
     */
    public void setDynamicProperty(String name, Object value) throws WrongValueException {
        if (name == null)
            throw new WrongValueException("name is required");
        if (!hasDynamicProperty(name))
            throw new WrongValueException("Attribute not allowed: " + name
                    + "\nSpecify the ZK namespace if you want to use special ZK attributes");

        String sval = Objects.toString(value);
        if ("style".equals(name)) {
            sval = filterStyle(sval);
            setDynaProp(name, sval);
        } else if ("src".equals(name)) {
            // ZK-3011: should defer until render
            EncodedURL url = new EncodedURL(sval);
            setDynaProp(name, url);
            sval = (String) url.getValue();
        } else if ("textContent".equals(name)) {
            setDynaProp(name, sval);
            if (!getChildren().isEmpty())
                invalidate();
        } else
            setDynaProp(name, value);
        // B80-ZK-2716: style and textContent are both dynamiccProperty
        smartUpdate("dynamicProperty", new String[] { name, sval }, true);
    }

    private String getEncodedURL(String src) {
        if (src == null)
            return Images.BASE64SPACERIMAGE;

        final Desktop dt = getDesktop(); // it might not belong to any desktop
        return dt != null ? dt.getExecution().encodeURL(src) : "";
    }

    /** Processes the style. */
    private String filterStyle(String style) {
        if (style != null) {
            final int j = HTMLs.getSubstyleIndex(style, "display");
            if (j >= 0) { // display is specified
                super.setVisible(!"none".equals(HTMLs.getSubstyleValue(style, j)));
                return style; // done
            }
        }

        if (!isVisible()) {
            int len = style != null ? style.length() : 0;
            if (len == 0)
                return "display:none;";
            if (style.charAt(len - 1) != ';')
                style += ';';
            style += "display:none;";
        }
        return style;
    }

    /** Set the dynamic property 'blindly'. */
    private void setDynaProp(String name, Object value) {
        if (value == null) {
            if (_props != null)
                _props.remove(name);
        } else {
            if (_props == null)
                _props = new LinkedHashMap<String, Object>();
            _props.put(name, value);
        }
    }

    /**
     * Whether to hide the id attribute.
     * <p>
     * Default: false.
     * <p>
     * Some tags, such as {@link org.zkoss.zhtml.Html}, won't generate the id attribute. They shall
     * override this method to return true.
     */
    protected boolean shallHideId() {
        return false;
    }

    // -- Component --//
    /**
     * Changes the visibility of this component.
     * <p>
     * Note: it will adjust the style ({@link #getStyle}) based on the visibility.
     *
     * @return the previous visibility
     */
    public boolean setVisible(boolean visible) {
        final boolean old = super.setVisible(visible);
        if (old != visible) {
            final String style = getStyle();
            if (visible) {
                if (style != null) {
                    final int j = HTMLs.getSubstyleIndex(style, "display");
                    if (j >= 0) {
                        final String val = HTMLs.getSubstyleValue(style, j);
                        if ("none".equals(val)) {
                            String newstyle = style.substring(0, j);
                            final int k = style.indexOf(';', j + 7);
                            if (k >= 0)
                                newstyle += style.substring(k + 1);
                            setDynaProp("style", newstyle);
                        }
                    }
                }
            } else {
                if (style == null) {
                    setDynaProp("style", "display:none;");
                } else {
                    final int j = HTMLs.getSubstyleIndex(style, "display");
                    if (j >= 0) {
                        final String val = HTMLs.getSubstyleValue(style, j);
                        if (!"none".equals(val)) {
                            String newstyle = style.substring(0, j) + "display:none;";
                            final int k = style.indexOf(';', j + 7);
                            if (k >= 0)
                                newstyle += style.substring(k + 1);
                            setDynaProp("style", newstyle);
                        }
                    } else {
                        final int len = style.length();
                        String newstyle = len > 0 && style.charAt(len - 1) != ';' ? style + ';'
                                : style;
                        setDynaProp("style", style + "display:none;");
                    }
                }
            }
        }
        return old;
    }

    /**
     * Returns the widget class, "zhtml.Widget".
     * 
     * @since 5.0.0
     */
    public String getWidgetClass() {
        return "zhtml.Widget";
    }

    public void redraw(java.io.Writer out) throws java.io.IOException {
        if (_tagnm == null)
            throw new UiException("The tag name is not initialized yet");

        final Execution exec = Executions.getCurrent();
        if (exec == null || exec.isAsyncUpdate(null) || !HtmlPageRenders.isDirectContent(exec)) {
            super.redraw(out); // generate JavaScript
            return;
        }

        TagRenderContext rc = PageRenderer.getTagRenderContext(exec);
        final boolean rcRequired = rc == null;
        Object ret = null;
        if (rcRequired) {
            ret = PageRenderer.beforeRenderTag(exec);
            rc = PageRenderer.getTagRenderContext(exec);
        }

        out.write(getPrologHalf(false));
        rc.renderBegin(this, getClientEvents(), getSpecialRendererOutput(this), false);

        redrawChildrenDirectly(rc, exec, out);

        out.write(getEpilogHalf());
        rc.renderEnd(this);

        if (rcRequired) {
            out.write(rc.complete());
            PageRenderer.afterRenderTag(exec, ret);
        }
    }

    /**
     * Renders the children directly to the given output. Notice it is called only if
     * {@link #redraw} is going to render the content (HTML tags) directly. If it is about to
     * generate the JavaScript code {@link #redrawChildren} will be called instead.
     * <p>
     * You have to override this method if the deriving class has additional information to render.
     * 
     * @since 5.0.7
     */
    protected void redrawChildrenDirectly(TagRenderContext rc, Execution exec, java.io.Writer out)
            throws java.io.IOException {
        for (Component child = getFirstChild(); child != null;) {
            Component next = child.getNextSibling();
            if (((ComponentCtrl) child).getExtraCtrl() instanceof DirectContent) {
                ((ComponentCtrl) child).redraw(out);
            } else {
                HtmlPageRenders.setDirectContent(exec, false);
                rc.renderBegin(child, null, getSpecialRendererOutput(child), true);
                HtmlPageRenders.outStandalone(exec, child, out);
                rc.renderEnd(child);
                HtmlPageRenders.setDirectContent(exec, true);
            }
            child = next;
        }
    }

    protected void renderProperties(org.zkoss.zk.ui.sys.ContentRenderer renderer)
            throws java.io.IOException {
        super.renderProperties(renderer);
        render(renderer, "prolog", getPrologHalf(false));
        render(renderer, "epilog", getEpilogHalf());
    }

    /**
     * @param hideUuidIfNoId
     *            whether not to generate UUID if possible
     */
    /* package */ String getPrologHalf(boolean hideUuidIfNoId) {
        final StringBuilder sb = new StringBuilder(128).append('<').append(_tagnm);

//        Do this at client side for ZK-5270
//        if ((!hideUuidIfNoId && !shallHideId()) || getId().length() > 0)
//            sb.append(" id=\"").append(getUuid()).append('"');

        if (_props != null) {
            for (Iterator it = _props.entrySet().iterator(); it.hasNext();) {
                final Map.Entry me = (Map.Entry) it.next();
                if (!"textContent".equals(me.getKey())) { // ignore textContent
                    // ZK-3011: should getValue if it's a deferredValue
                    Object v = me.getValue();
                    if (v instanceof DeferredValue) {
                        v = ((DeferredValue) v).getValue();
                    }
                    sb.append(' ').append(me.getKey()).append("=\"")
                            .append(XMLs.encodeAttribute(Objects.toString(v))).append('"');
                }
            }
        }

        if (!isOrphanTag())
            sb.append('/');

        sb.append('>');

        Object textContent = getDynamicProperty("textContent");
        if (textContent != null)
            sb.append(XMLs.escapeXML((String) textContent));
        return sb.toString();
    }

    /* package */ String getEpilogHalf() {
        return isOrphanTag() ? "</" + _tagnm + '>' : "";
    }

    protected boolean isChildable() {
        return isOrphanTag();
    }

    /**
     * Returns whether this tag is an orphan tag, i.e., it shall be in the form of &lt;tag/&gt;.
     * 
     * @since 5.0.8
     */
    protected boolean isOrphanTag() {
        return !HTMLs.isOrphanTag(_tagnm);
    }

    // --ComponentCtrl--//
    private static HashMap<String, PropertyAccess> _properties = new HashMap<String, PropertyAccess>(
            5);

    static {
        _properties.put("id", new StringPropertyAccess() {
            public void setValue(Component cmp, String value) {
                ((AbstractTag) cmp).setId(value);
            }

            public String getValue(Component cmp) {
                return ((AbstractTag) cmp).getId();
            }
        });
        _properties.put("sclass", new StringPropertyAccess() {
            public void setValue(Component cmp, String value) {
                ((AbstractTag) cmp).setSclass(value);
            }

            public String getValue(Component cmp) {
                return ((AbstractTag) cmp).getSclass();
            }
        });
        _properties.put("style", new StringPropertyAccess() {
            public void setValue(Component cmp, String value) {
                ((AbstractTag) cmp).setStyle(value);
            }

            public String getValue(Component cmp) {
                return ((AbstractTag) cmp).getStyle();
            }
        });
        _properties.put("visible", new BooleanPropertyAccess() {
            public void setValue(Component cmp, Boolean value) {
                ((AbstractTag) cmp).setVisible(value);
            }

            public Boolean getValue(Component cmp) {
                return ((AbstractTag) cmp).isVisible();
            }
        });
    }

    public PropertyAccess getPropertyAccess(String prop) {
        PropertyAccess pa = _properties.get(prop);
        if (pa != null)
            return pa;
        return super.getPropertyAccess(prop);
    }

    // Cloneable//
    public Object clone() {
        final AbstractTag clone = (AbstractTag) super.clone();
        if (clone._props != null)
            clone._props = new LinkedHashMap<String, Object>(clone._props);
        return clone;
    }

    // Object//
    public String toString() {
        return "[" + _tagnm + ' ' + super.toString() + ']';
    }

    public Object getExtraCtrl() {
        return new ExtraCtrl();
    }

    protected class ExtraCtrl implements DirectContent {
    }

    // ZK-3097
    private class EncodedURL implements org.zkoss.zk.au.DeferredValue, Serializable {
        private String _src;

        public EncodedURL(String src) {
            _src = src;
        }

        public Object getValue() {
            return getEncodedURL(_src);
        }
    }
}