/** * 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.model; import org.orbeon.dom.*; import org.orbeon.oxf.common.OXFException; import org.orbeon.oxf.xforms.XFormsUtils; import org.orbeon.oxf.xforms.analysis.model.Model; import org.orbeon.oxf.xforms.model.BindNode; import org.orbeon.oxf.xml.XMLConstants; import org.orbeon.oxf.xml.dom4j.Dom4jUtils; import org.orbeon.oxf.xml.dom4j.LocationData; import org.orbeon.saxon.om.*; import java.util.*; /** * Instances of this class are used to annotate XForms instance nodes with MIPs and other information. * * Previously, all element and attribute nodes in every XForms instance were annotated with instances of this class. * Annotations are now done lazily when needed in order to reduce the number of objects created. This has a positive * impact on memory usage and garbage collection. This is also why most methods in this class are static. * * Since 2010-12, this now points back to bind nodes, which store bind MIPs directly. */ public class InstanceData {// rename to DataNodeProperties once done private LocationData locationData; // Point back to binds that impacted this node private List<BindNode> bindNodes; // Types set by schema or binds private QName bindType; private QName schemaType; // Schema validity: only set by schema private boolean schemaInvalid; // Whether to evaluate the default value public boolean requireDefaultValue; // Annotations (used only for multipart submission as of 2010-12) private Map<String, String> transientAnnotations; public List<BindNode> getBindNodes() { if (bindNodes == null) return Collections.emptyList(); else return bindNodes; } public static void addBindNode(NodeInfo nodeInfo, BindNode bindNode) { final InstanceData instanceData = getOrCreateInstanceData(nodeInfo, false); if (instanceData != READONLY_LOCAL_INSTANCE_DATA) { // only register ourselves if we are not a readonly node if (instanceData.bindNodes == null) instanceData.bindNodes = Collections.singletonList(bindNode); else if (instanceData.bindNodes.size() == 1) { final BindNode oldBindNode = instanceData.bindNodes.get(0); instanceData.bindNodes = new ArrayList<BindNode>(4); // hoping that situations where many binds point to same node are rare instanceData.bindNodes.add(oldBindNode); instanceData.bindNodes.add(bindNode); } else { instanceData.bindNodes.add(bindNode); } } } private static final InstanceData READONLY_LOCAL_INSTANCE_DATA = new InstanceData() { @Override public boolean getLocalRelevant() { return Model.DEFAULT_RELEVANT(); } @Override public boolean getLocalReadonly() { return true; } @Override public boolean getRequired() { return Model.DEFAULT_REQUIRED(); } @Override public boolean getValid() { return Model.DEFAULT_VALID(); } @Override public QName getSchemaOrBindType() { return null; } @Override public String getInvalidBindIds() { return null; } @Override public LocationData getLocationData() { return null; } }; public boolean getLocalRelevant() { if (bindNodes != null && bindNodes.size() > 0) for (final BindNode bindNode : bindNodes) if (bindNode.relevant() != Model.DEFAULT_RELEVANT()) return !Model.DEFAULT_RELEVANT(); return Model.DEFAULT_RELEVANT(); } public boolean getLocalReadonly() { if (bindNodes != null && bindNodes.size() > 0) for (final BindNode bindNode : bindNodes) if (bindNode.readonly() != Model.DEFAULT_READONLY()) return !Model.DEFAULT_READONLY(); return Model.DEFAULT_READONLY(); } public boolean getRequired() { if (bindNodes != null && bindNodes.size() > 0) for (final BindNode bindNode : bindNodes) if (bindNode.required() != Model.DEFAULT_REQUIRED()) return !Model.DEFAULT_REQUIRED(); return Model.DEFAULT_REQUIRED(); } public boolean getValid() { if (schemaInvalid) return false; if (bindNodes != null && bindNodes.size() > 0) for (final BindNode bindNode : bindNodes) if (bindNode.valid() != Model.DEFAULT_VALID()) return !Model.DEFAULT_VALID(); return Model.DEFAULT_VALID(); } public scala.collection.immutable.Map<String, String> collectAllCustomMIPs() { return BindNode.collectAllCustomMIPs(bindNodes); } public QName getSchemaOrBindType() { if (schemaType != null) return schemaType; return bindType; } public String getInvalidBindIds() { StringBuilder sb = null; if (bindNodes != null && bindNodes.size() > 0) for (final BindNode bindNode : bindNodes) if (bindNode.valid() != Model.DEFAULT_VALID()) { if (sb == null) sb = new StringBuilder(); else if (sb.length() > 0) sb.append(' '); sb.append(bindNode.parentBind().staticId()); } return sb == null ? null : sb.toString(); } public void setTransientAnnotation(String name, String value) { if (transientAnnotations == null) transientAnnotations = new HashMap<String, String>(); transientAnnotations.put(name, value); } public String getTransientAnnotation(String name) { return (transientAnnotations == null) ? null : transientAnnotations.get(name); } public static void setTransientAnnotation(NodeInfo nodeInfo, String name, String value) { final InstanceData instanceData = getOrCreateInstanceData(nodeInfo, true); instanceData.setTransientAnnotation(name,value); } public static String getTransientAnnotation(Node node, String name) { final InstanceData existingInstanceData = getLocalInstanceData(node); return (existingInstanceData == null) ? null : existingInstanceData.getTransientAnnotation(name); } public static scala.collection.immutable.Map<String, String> collectAllCustomMIPs(NodeInfo nodeInfo) { final InstanceData existingInstanceData = getLocalInstanceData(nodeInfo, false); return (existingInstanceData == null) ? null : existingInstanceData.collectAllCustomMIPs(); } public static boolean getInheritedRelevant(NodeInfo nodeInfo) { if (nodeInfo instanceof VirtualNode) { return getInheritedRelevant(XFormsUtils.getNodeFromNodeInfo(nodeInfo, "")); } else if (nodeInfo != null) { return Model.DEFAULT_RELEVANT(); } else { throw new OXFException("Cannot get relevant Model Item Property on null object."); } } public static boolean getInheritedRelevant(Node node) { // Iterate this node and its parents. The node is non-relevant if it or any ancestor is non-relevant. for (Node currentNode = node; currentNode != null; currentNode = currentNode.getParent()) { final InstanceData currentInstanceData = getLocalInstanceData(currentNode); final boolean currentRelevant = (currentInstanceData == null) ? Model.DEFAULT_RELEVANT() : currentInstanceData.getLocalRelevant(); if (!currentRelevant) return false; } return true; } public static boolean getRequired(NodeInfo nodeInfo) { final InstanceData existingInstanceData = getLocalInstanceData(nodeInfo, false); return (existingInstanceData == null) ? Model.DEFAULT_REQUIRED() : existingInstanceData.getRequired(); } public static boolean getRequired(Node node) { final InstanceData existingInstanceData = getLocalInstanceData(node); return (existingInstanceData == null) ? Model.DEFAULT_REQUIRED() : existingInstanceData.getRequired(); } public static boolean getInheritedReadonly(NodeInfo nodeInfo) { if (nodeInfo instanceof VirtualNode) { return getInheritedReadonly(XFormsUtils.getNodeFromNodeInfo(nodeInfo, "")); } else if (nodeInfo != null) { return true;// Default for non-mutable nodes is to be read-only } else { throw new OXFException("Cannot get readonly Model Item Property on null object."); } } public static boolean getInheritedReadonly(Node node) { // Iterate this node and its parents. The node is readonly if it or any ancestor is readonly. for (Node currentNode = node; currentNode != null; currentNode = currentNode.getParent()) { final InstanceData currentInstanceData = getLocalInstanceData(currentNode); final boolean currentReadonly = (currentInstanceData == null) ? Model.DEFAULT_READONLY() : currentInstanceData.getLocalReadonly(); if (currentReadonly) return true; } return false; } public static boolean getValid(NodeInfo nodeInfo) { final InstanceData existingInstanceData = getLocalInstanceData(nodeInfo, false); return (existingInstanceData == null) ? Model.DEFAULT_VALID() : existingInstanceData.getValid(); } public static boolean getValid(Node node) { final InstanceData existingInstanceData = getLocalInstanceData(node); return (existingInstanceData == null) ? Model.DEFAULT_VALID() : existingInstanceData.getValid(); } public static void setBindType(NodeInfo nodeInfo, QName type) { getOrCreateInstanceData(nodeInfo, true).bindType = type; } public static void setSchemaType(Node node, QName type) { getOrCreateInstanceData(node).schemaType = type; } public static QName getType(NodeInfo nodeInfo) { // Try schema or bind type final InstanceData existingInstanceData = getLocalInstanceData(nodeInfo, false); if (existingInstanceData != null) { final QName schemaOrBindType = existingInstanceData.getSchemaOrBindType(); if (schemaOrBindType != null) return schemaOrBindType; } // No type was assigned by schema or MIP, try xsi:type if (nodeInfo.getNodeKind() == org.w3c.dom.Node.ELEMENT_NODE) { // Check for xsi:type attribute // NOTE: Saxon 9 has new code to resolve such QNames final String typeQName = nodeInfo.getAttributeValue(StandardNames.XSI_TYPE); if (typeQName != null) { try { final NameChecker checker = nodeInfo.getConfiguration().getNameChecker(); final String[] parts = checker.getQNameParts(typeQName); // No prefix if (parts[0].equals("")) { return QName.get(parts[1]); } // There is a prefix, resolve it final SequenceIterator namespaceNodes = nodeInfo.iterateAxis(Axis.NAMESPACE); while (true) { final NodeInfo currentNamespaceNode = (NodeInfo) namespaceNodes.next(); if (currentNamespaceNode == null) { break; } final String prefix = currentNamespaceNode.getLocalPart(); if (prefix.equals(parts[0])) { return QName.get(parts[1], "", currentNamespaceNode.getStringValue()); } } } catch (Exception e) { throw new OXFException(e); } } } return null; } public static QName getType(Node node) { // Try schema or bind type final InstanceData existingInstanceData = getLocalInstanceData(node); if (existingInstanceData != null) { final QName schemaOrBindType = existingInstanceData.getSchemaOrBindType(); if (schemaOrBindType != null) return schemaOrBindType; } // No type was assigned by schema or MIP, try xsi:type if (node instanceof Element) { // Check for xsi:type attribute final Element element = (Element) node; return Dom4jUtils.extractAttributeValueQName(element, XMLConstants.XSI_TYPE_QNAME, false); // TODO: should pass true? } return null; } public static String getInvalidBindIds(NodeInfo nodeInfo) { final InstanceData existingInstanceData = getLocalInstanceData(nodeInfo, false); return (existingInstanceData == null) ? null : existingInstanceData.getInvalidBindIds(); } public static void setRequireDefaultValue(Node node) { final InstanceData instanceData = getOrCreateInstanceData(node); instanceData.requireDefaultValue = true; } public static void clearRequireDefaultValue(Node node) { final InstanceData instanceData = getLocalInstanceData(node); if (instanceData != null) instanceData.requireDefaultValue = false; } public static boolean getRequireDefaultValue(NodeInfo nodeInfo) { final InstanceData existingInstanceData = getLocalInstanceData(nodeInfo, false); return existingInstanceData.requireDefaultValue; } public static void removeInstanceData(Node node) { if (node instanceof Element) { ((Element) node).setData(null); } else if (node instanceof Attribute) { ((Attribute) node).setData(null); } } public static void addSchemaError(Node node) { // Get or create InstanceData final InstanceData instanceData = getOrCreateInstanceData(node); // Remember that the value is invalid instanceData.schemaInvalid = true; } public static void clearStateForRebuild(NodeInfo nodeInfo) { final InstanceData existingInstanceData = getLocalInstanceData(nodeInfo, false);// not really an update since for read-only nothing changes if (existingInstanceData != null) { existingInstanceData.bindNodes = null; existingInstanceData.bindType = null; existingInstanceData.schemaType = null; existingInstanceData.schemaInvalid = false; existingInstanceData.transientAnnotations = null; // NOTE: Do not clear requireInitialData which must survive a rebuild } } public static void clearSchemaState(NodeInfo nodeInfo) { final InstanceData existingInstanceData = getLocalInstanceData(nodeInfo, false);// not really an update since for read-only nothing changes if (existingInstanceData != null) { existingInstanceData.schemaType = null; existingInstanceData.schemaInvalid = false; } } private static InstanceData getOrCreateInstanceData(NodeInfo nodeInfo, boolean forUpdate) { final InstanceData existingInstanceData = getLocalInstanceData(nodeInfo, forUpdate); return (existingInstanceData != null) ? existingInstanceData : createNewInstanceData(nodeInfo); } private static InstanceData getOrCreateInstanceData(Node node) { final InstanceData existingInstanceData = getLocalInstanceData(node); return (existingInstanceData != null) ? existingInstanceData : createNewInstanceData(node); } public static InstanceData getLocalInstanceData(NodeInfo nodeInfo, boolean forUpdate) { if (nodeInfo instanceof VirtualNode) { return getLocalInstanceData(XFormsUtils.getNodeFromNodeInfo(nodeInfo, "")); } else if (nodeInfo != null && ! forUpdate) { return READONLY_LOCAL_INSTANCE_DATA; } else if (nodeInfo != null && forUpdate) { throw new OXFException("Cannot update MIP information on non-VirtualNode NodeInfo."); } else { throw new OXFException("Null NodeInfo found."); } } public static InstanceData getLocalInstanceData(Node node) { // Find data annotation on node final Object instanceData; if (node instanceof Element) { instanceData = ((Element) node).getData(); } else if (node instanceof Attribute) { instanceData = ((Attribute) node).getData(); } else if (node instanceof Document) { // We can't store data on the Document object. Use root element instead. instanceData = ((Document) node).getRootElement().getData(); } else { // TODO: other node types once we update to handling text nodes correctly. But it looks like Text does not support data. return null; } // Make sure we return InstanceData and not something else if (instanceData instanceof InstanceData) return (InstanceData) instanceData; else return null; } private static InstanceData createNewInstanceData(NodeInfo nodeInfo) { if (nodeInfo instanceof VirtualNode) { return createNewInstanceData(XFormsUtils.getNodeFromNodeInfo(nodeInfo, "")); } else { throw new OXFException("Cannot create InstanceData on non-VirtualNode NodeInfo."); } } private static InstanceData createNewInstanceData(Node node) { final InstanceData instanceData; if (node instanceof Element) { final Element element = (Element) node; instanceData = InstanceData.createNewInstanceData(element.getData()); element.setData(instanceData); } else if (node instanceof Attribute) { final Attribute attribute = (Attribute) node; instanceData = InstanceData.createNewInstanceData(attribute.getData()); attribute.setData(instanceData); } else if (node instanceof Document) { // We can't store data on the Document object. Use root element instead. final Element element = ((Document) node).getRootElement(); instanceData = InstanceData.createNewInstanceData(element.getData()); element.setData(instanceData); } else { // No other node type is supported throw new OXFException("Cannot create InstanceData on node type: " + Node$.MODULE$.nodeTypeName(node)); } return instanceData; } private static InstanceData createNewInstanceData(Object existingData) { if (existingData instanceof LocationData) { return new InstanceData((LocationData) existingData); } else if (existingData instanceof InstanceData) { return new InstanceData(((InstanceData) existingData).getLocationData()); } else { return new InstanceData(null); } } private InstanceData() {} private InstanceData(LocationData locationData) { this.locationData = locationData; } public LocationData getLocationData() { return locationData; } }