org.orbeon.oxf.xforms.control.XFormsSingleNodeControl.java Source code

Java tutorial

Introduction

Here is the source code for org.orbeon.oxf.xforms.control.XFormsSingleNodeControl.java

Source

/**
 * Copyright (C) 2010 Orbeon, Inc.
 *
 * 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
 * 2.1 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 Lesser General Public License for more details.
 *
 * The full text of the license is available at http://www.gnu.org/copyleft/lesser.html
 */
package org.orbeon.oxf.xforms.control;

import org.dom4j.Element;
import org.dom4j.QName;
import org.orbeon.oxf.xforms.*;
import org.orbeon.oxf.xforms.analysis.controls.SingleNodeTrait;
import org.orbeon.oxf.xforms.control.controls.XFormsUploadControl;
import org.orbeon.oxf.xforms.xbl.XBLContainer;
import org.orbeon.oxf.xml.ContentHandlerHelper;
import org.orbeon.oxf.xml.XMLConstants;
import org.orbeon.oxf.xml.dom4j.Dom4jUtils;
import org.orbeon.saxon.om.Item;
import org.orbeon.saxon.om.NodeInfo;
import org.xml.sax.Attributes;
import org.xml.sax.helpers.AttributesImpl;
import scala.collection.immutable.Seq;

import java.util.HashMap;
import java.util.Map;

/**
 * Control with a single-node binding (possibly optional). Such controls can have MIPs.
 * 
 * @noinspection SimplifiableIfStatement
 */
public abstract class XFormsSingleNodeControl extends XFormsControl {

    // Bound item
    private Item boundItem;

    // Standard MIPs
    private boolean readonly;
    private boolean required;
    private boolean valid = true;

    // Previous values for refresh
    private boolean wasReadonly;
    private boolean wasRequired;
    private boolean wasValid = true;

    // Type
    private QName type;

    // Custom MIPs
    private Map<String, String> customMIPs;
    private String customMIPsAsString;

    public XFormsSingleNodeControl(XBLContainer container, XFormsControl parent, Element element,
            String effectiveId) {
        super(container, parent, element, effectiveId);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        // Set default MIPs so that diff picks up the right values
        setDefaultMIPs();
    }

    @Override
    public void onCreate() {
        super.onCreate();

        readBinding();

        wasReadonly = false;
        wasRequired = false;
        wasValid = true;
    }

    @Override
    public void onBindingUpdate(BindingContext oldBinding, BindingContext newBinding) {
        super.onBindingUpdate(oldBinding, newBinding);
        readBinding();
    }

    private void readBinding() {
        // Set bound item, only considering actual bindings (with @bind, @ref or @nodeset)
        if (bindingContext().isNewBind())
            this.boundItem = bindingContext().getSingleItem();

        // Get MIPs
        final Item currentItem = getBoundItem();
        if (currentItem != null) {
            if (currentItem instanceof NodeInfo) {
                // Control is bound to a node - get model item properties
                final NodeInfo currentNodeInfo = (NodeInfo) currentItem;
                this.readonly = InstanceData.getInheritedReadonly(currentNodeInfo);
                this.required = InstanceData.getRequired(currentNodeInfo);
                this.valid = InstanceData.getValid(currentNodeInfo);
                this.type = InstanceData.getType(currentNodeInfo);

                // Custom MIPs
                final Map<String, String> tempCustomMIPs = InstanceData.getAllCustom(currentNodeInfo);
                if (tempCustomMIPs != null)
                    this.customMIPs = new HashMap<String, String>(tempCustomMIPs);

                // Handle global read-only setting
                if (XFormsProperties.isReadonly(containingDocument()))
                    this.readonly = true;
            } else {
                // Control is not bound to a node (i.e. bound to an atomic value)
                setAtomicValueMIPs();
            }
        } else {
            // Control is not bound to a node because it doesn't have a binding (group, trigger, dialog, etc. without @ref)
            setDefaultMIPs();
        }
    }

    private void setAtomicValueMIPs() {
        setDefaultMIPs();
        this.readonly = true;
    }

    private void setDefaultMIPs() {
        this.readonly = false;
        this.required = false;
        this.valid = true;// by default, a control is not invalid
        this.type = null;
        this.customMIPs = null;
        this.customMIPsAsString = null;
    }

    @Override
    public void commitCurrentUIState() {
        super.commitCurrentUIState();

        isValueChanged();
        wasRequired();
        wasReadonly();
        wasValid();
    }

    @Override
    public Seq<Item> binding() {
        return scala.Option.apply(boundItem).toList();
    }

    /**
     * Return the item (usually a node) to which the control is bound, if any. If the control is not bound to any item,
     * return null.
     *
     * @return bound item or null
     */
    public final Item getBoundItem() {
        return boundItem;
    }

    public final boolean isReadonly() {
        return readonly;
    }

    /**
     * Convenience method to figure out when a control is relevant, assuming a "null" control is non-relevant.
     *
     * @param control   control to test or not
     * @return          true if the control is not null and it is relevant
     */
    public static boolean isRelevant(XFormsSingleNodeControl control) {
        return control != null && control.isRelevant();
    }

    @Override
    public boolean supportsRefreshEvents() {
        // Single-node controls support refresh events
        return true;
    }

    public final boolean isRequired() {
        return required;
    }

    public final boolean wasReadonly() {
        final boolean result = wasReadonly;
        wasReadonly = readonly;
        return result;
    }

    public final boolean wasRequired() {
        final boolean result = wasRequired;
        wasRequired = required;
        return result;
    }

    public final boolean wasValid() {
        final boolean result = wasValid;
        wasValid = valid;
        return result;
    }

    public boolean isValueChanged() {
        return false;
    }

    public QName getType() {
        return type;
    }

    public String getTypeExplodedQName() {
        return Dom4jUtils.qNameToExplodedQName(type);
    }

    public Map<String, String> getCustomMIPs() {
        return customMIPs;
    }

    /**
     * Return this control's custom MIPs as a String containing space-separated classes.
     *
     * @return  classes, or null if no custom MIPs
     */
    public String getCustomMIPsClasses() {
        final Map<String, String> customMIPs = getCustomMIPs();
        if (customMIPs != null) {
            // There are custom MIPs
            if (customMIPsAsString == null) {
                // Must compute now

                final StringBuilder sb = new StringBuilder(20);

                for (final Map.Entry<String, String> entry : customMIPs.entrySet()) {
                    final String name = entry.getKey();
                    final String value = entry.getValue();

                    if (sb.length() > 0)
                        sb.append(' ');

                    // TODO: encode so that there are no spaces
                    sb.append(name);
                    sb.append('-');
                    sb.append(value);
                }
                customMIPsAsString = sb.toString();
            }
        } else {
            // No custom MIPs so string value is null
        }

        return customMIPsAsString;
    }

    /**
     * Convenience method to return the local name of a built-in XML Schema or XForms type.
     *
     * @return the local name of the built-in type, or null if not found
     */
    public String getBuiltinTypeName() {
        final QName type = getType();

        if (type != null) {
            final boolean isBuiltInSchemaType = type.getNamespaceURI().equals(XMLConstants.XSD_URI);
            final boolean isBuiltInXFormsType = type.getNamespaceURI().equals(XFormsConstants.XFORMS_NAMESPACE_URI);

            if (isBuiltInSchemaType || isBuiltInXFormsType) {
                return type.getName();
            } else {
                return null;
            }
        } else {
            return null;
        }
    }

    /**
     * Convenience method to return the local name of the XML Schema type.
     *
     * @return the local name of the type, or null if not found
     */
    public String getTypeLocalName() {
        final QName type = getType();

        if (type != null) {
            return type.getName();
        } else {
            return null;
        }
    }

    public boolean isValid() {
        return valid;
    }

    @Override
    public boolean computeRelevant() {

        // If parent is not relevant then we are not relevant either
        if (!super.computeRelevant())
            return false;

        final Item currentItem = bindingContext().getSingleItem();
        if (bindingContext().isNewBind()) {
            // There is a binding
            if (isAllowedBoundItem(currentItem)) {
                if (currentItem instanceof NodeInfo) {
                    final NodeInfo currentNodeInfo = (NodeInfo) currentItem;
                    // Control is bound to an acceptable node, get node relevance
                    return InstanceData.getInheritedRelevant(currentNodeInfo);
                } else {
                    // Control bound to a value is considered relevant
                    return true;
                }
            } else {
                // Q: Should warn?
                return false;
            }
        } else {
            // Control is not bound because it doesn't have a binding (group, trigger, dialog, etc. without @ref)
            return true;
        }
    }

    // Allow override only for dangling XFormsOutputControl
    protected boolean isAllowedBoundItem(Item item) {
        return ((SingleNodeTrait) staticControl()).isAllowedBoundItem(item);
    }

    @Override
    public boolean equalsExternal(XFormsControl other) {

        if (other == null || !(other instanceof XFormsSingleNodeControl))
            return false;

        if (this == other)
            return true;

        final XFormsSingleNodeControl otherSingleNodeControl = (XFormsSingleNodeControl) other;

        // Standard MIPs
        if (readonly != otherSingleNodeControl.readonly)
            return false;
        if (required != otherSingleNodeControl.required)
            return false;
        if (valid != otherSingleNodeControl.valid)
            return false;

        // Custom MIPs
        if (!compareCustomMIPs(customMIPs, otherSingleNodeControl.customMIPs))
            return false;

        // Type
        if (!XFormsUtils.compareStrings(type, otherSingleNodeControl.type))
            return false;

        return super.equalsExternal(other);
    }

    public static boolean compareCustomMIPs(Map mips1, Map mips2) {
        // equal if the mappings are equal, so Map equality should work fine
        return mips1 == null && mips2 == null || mips1 != null && mips1.equals(mips2);
    }

    @Override
    public boolean isStaticReadonly() {
        // Static read-only if we are read-only and static (global or local setting)
        return isReadonly() && hasStaticReadonlyAppearance();
    }

    public boolean hasStaticReadonlyAppearance() {
        return XFormsProperties.isStaticReadonlyAppearance(containingDocument())
                || XFormsProperties.READONLY_APPEARANCE_STATIC_VALUE.equals(
                        element().attributeValue(XFormsConstants.XXFORMS_READONLY_APPEARANCE_ATTRIBUTE_QNAME));
    }

    @Override
    public boolean setFocus() {
        return Focus.focusWithEvents(this);
    }

    @Override
    public void outputAjaxDiff(ContentHandlerHelper ch, XFormsControl other, AttributesImpl attributesImpl,
            boolean isNewlyVisibleSubtree) {

        assert attributesImpl.getLength() == 0;

        final XFormsSingleNodeControl control1 = (XFormsSingleNodeControl) other;
        final XFormsSingleNodeControl control2 = this;

        // Add attributes
        final boolean doOutputElement = addAjaxAttributes(attributesImpl, isNewlyVisibleSubtree, other);

        // Get current value if possible for this control
        // NOTE: We issue the new value in all cases because we don't have yet a mechanism to tell the
        // client not to update the value, unlike with attributes which can be omitted
        if (control2 instanceof XFormsValueControl && !(control2 instanceof XFormsUploadControl)) {

            // TODO: Output value only when changed

            // Output element
            final XFormsValueControl xformsValueControl = (XFormsValueControl) control2;
            outputValueElement(ch, xformsValueControl, doOutputElement, isNewlyVisibleSubtree, attributesImpl,
                    "control");
        } else {
            // No value, just output element with no content (but there may be attributes)
            if (doOutputElement)
                ch.element("xxf", XFormsConstants.XXFORMS_NAMESPACE_URI, "control", attributesImpl);
        }

        // Output extension attributes in no namespace
        // TODO: If only some attributes changed, then we also output xxf:control above, which is unnecessary
        control2.addAjaxStandardAttributes(control1, ch, isNewlyVisibleSubtree);
    }

    @Override
    public boolean addAjaxAttributes(AttributesImpl attributesImpl, boolean isNewlyVisibleSubtree,
            XFormsControl other) {

        final XFormsSingleNodeControl control1 = (XFormsSingleNodeControl) other;
        final XFormsSingleNodeControl control2 = this;

        // Call base class for the standard stuff
        boolean added = super.addAjaxAttributes(attributesImpl, isNewlyVisibleSubtree, other);

        // MIPs
        added |= addAjaxMIPs(attributesImpl, isNewlyVisibleSubtree, control1, control2);

        return added;
    }

    private boolean addAjaxMIPs(AttributesImpl attributesImpl, boolean isNewlyVisibleSubtree,
            XFormsSingleNodeControl control1, XFormsSingleNodeControl control2) {

        boolean added = false;
        if (isNewlyVisibleSubtree && control2.isReadonly()
                || control1 != null && control1.isReadonly() != control2.isReadonly()) {
            attributesImpl.addAttribute("", XFormsConstants.READONLY_ATTRIBUTE_NAME,
                    XFormsConstants.READONLY_ATTRIBUTE_NAME, ContentHandlerHelper.CDATA,
                    Boolean.toString(control2.isReadonly()));
            added = true;
        }
        if (isNewlyVisibleSubtree && control2.isRequired()
                || control1 != null && control1.isRequired() != control2.isRequired()) {
            attributesImpl.addAttribute("", XFormsConstants.REQUIRED_ATTRIBUTE_NAME,
                    XFormsConstants.REQUIRED_ATTRIBUTE_NAME, ContentHandlerHelper.CDATA,
                    Boolean.toString(control2.isRequired()));
            added = true;
        }

        // NOTE: We used to have a configurable default for the relevance. Not sure why this was needed. Here consider the default is true.
        if (isNewlyVisibleSubtree && !control2.isRelevant()
                //|| XFormsSingleNodeControl.isRelevant(xformsSingleNodeControl1) != XFormsSingleNodeControl.isRelevant(xformsSingleNodeControl2)) {
                || control1 != null && control1.isRelevant() != control2.isRelevant()) {//TODO: not sure why the above alternative fails tests. Which is more correct?
            attributesImpl.addAttribute("", XFormsConstants.RELEVANT_ATTRIBUTE_NAME,
                    XFormsConstants.RELEVANT_ATTRIBUTE_NAME, ContentHandlerHelper.CDATA,
                    Boolean.toString(control2.isRelevant()));
            added = true;
        }
        if (isNewlyVisibleSubtree && !control2.isValid()
                || control1 != null && control1.isValid() != control2.isValid()) {
            attributesImpl.addAttribute("", XFormsConstants.VALID_ATTRIBUTE_NAME,
                    XFormsConstants.VALID_ATTRIBUTE_NAME, ContentHandlerHelper.CDATA,
                    Boolean.toString(control2.isValid()));
            added = true;
        }

        // Type attribute
        {
            final String typeValue1 = isNewlyVisibleSubtree ? null : control1.getTypeExplodedQName();
            final String typeValue2 = control2.getTypeExplodedQName();

            if (isNewlyVisibleSubtree || !XFormsUtils.compareStrings(typeValue1, typeValue2)) {
                final String attributeValue = typeValue2 != null ? typeValue2 : "";
                // NOTE: No type is considered equivalent to xs:string or xforms:string
                // TODO: should have more generic code in XForms engine to equate "no type" and "xs:string"
                added |= AjaxSupport.addOrAppendToAttributeIfNeeded(attributesImpl, "type", attributeValue,
                        isNewlyVisibleSubtree,
                        attributeValue.equals("") || XMLConstants.XS_STRING_EXPLODED_QNAME.equals(attributeValue)
                                || XFormsConstants.XFORMS_STRING_EXPLODED_QNAME.equals(attributeValue));
            }
        }

        // Custom MIPs
        added |= addAjaxCustomMIPs(attributesImpl, isNewlyVisibleSubtree, control1, control2);

        return added;
    }

    protected void outputValueElement(ContentHandlerHelper ch, XFormsValueControl xformsValueControl,
            boolean doOutputElement, boolean isNewlyVisibleSubtree, Attributes attributesImpl, String elementName) {
        // Create element with text value
        final String value;
        if (xformsValueControl.isRelevant()) {
            // NOTE: Not sure if it is still possible to have a null value when the control is relevant
            final String tempValue = xformsValueControl.getEscapedExternalValue();
            value = (tempValue == null) ? "" : tempValue;
        } else {
            // Some controls don't have "" as non-relevant value
            value = xformsValueControl.getNonRelevantEscapedExternalValue();
        }
        if (doOutputElement || !isNewlyVisibleSubtree || !value.equals("")) {
            ch.startElement("xxf", XFormsConstants.XXFORMS_NAMESPACE_URI, elementName, attributesImpl);
            if (value.length() > 0)
                ch.text(value);
            ch.endElement();
        }
    }

    // public for unit tests
    public static boolean addAjaxCustomMIPs(AttributesImpl attributesImpl, boolean newlyVisibleSubtree,
            XFormsSingleNodeControl control1, XFormsSingleNodeControl control2) {

        boolean added = false;

        final Map<String, String> customMIPs1 = (control1 == null) ? null : control1.getCustomMIPs();
        final Map<String, String> customMIPs2 = control2.getCustomMIPs();

        if (newlyVisibleSubtree || !XFormsSingleNodeControl.compareCustomMIPs(customMIPs1, customMIPs2)) {
            // Custom MIPs changed

            final String attributeValue;
            if (customMIPs1 == null) {
                attributeValue = control2.getCustomMIPsClasses();
            } else {
                final StringBuilder sb = new StringBuilder(100);

                // Classes to remove
                for (final Map.Entry<String, String> entry : customMIPs1.entrySet()) {
                    final String name = entry.getKey();
                    final String value = entry.getValue();

                    // customMIPs2 may be null if the control becomes no longer bound
                    final String newValue = (customMIPs2 == null) ? null : customMIPs2.get(name);
                    if (newValue == null || !value.equals(newValue)) {

                        if (sb.length() > 0)
                            sb.append(' ');

                        sb.append('-');
                        // TODO: encode so that there are no spaces
                        sb.append(name);
                        sb.append('-');
                        sb.append(value);
                    }
                }

                // Classes to add
                // customMIPs2 may be null if the control becomes no longer bound
                if (customMIPs2 != null) {
                    for (final Map.Entry<String, String> entry : customMIPs2.entrySet()) {
                        final String name = entry.getKey();
                        final String value = entry.getValue();

                        final String oldValue = customMIPs1.get(name);
                        if (oldValue == null || !value.equals(oldValue)) {

                            if (sb.length() > 0)
                                sb.append(' ');

                            sb.append('+');
                            // TODO: encode so that there are no spaces
                            sb.append(name);
                            sb.append('-');
                            sb.append(value);
                        }
                    }
                }

                attributeValue = sb.toString();
            }
            // This attribute is a space-separate list of class names prefixed with either '-' or '+'
            if (attributeValue != null)
                added |= AjaxSupport.addOrAppendToAttributeIfNeeded(attributesImpl, "class", attributeValue,
                        newlyVisibleSubtree, attributeValue.equals(""));
        }
        return added;
    }
}