/* * eXist Open Source Native XML Database * Copyright (C) 2001-2014 The eXist-db Project * http://exist-db.org * * 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 * 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. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ package org.exist.dom.persistent; import org.exist.EXistException; import org.exist.Namespaces; import org.exist.dom.NamedNodeMapImpl; import org.exist.dom.QName; import org.exist.indexing.IndexController; import org.exist.indexing.StreamListener; import org.exist.indexing.StreamListener.ReindexMode; import org.exist.numbering.NodeId; import org.exist.stax.ExtendedXMLStreamReader; import org.exist.stax.IEmbeddedXMLStreamReader; import org.exist.storage.*; import org.exist.storage.btree.Value; import org.exist.storage.dom.INodeIterator; import org.exist.storage.txn.TransactionException; import org.exist.storage.txn.TransactionManager; import org.exist.storage.txn.Txn; import org.exist.util.ByteArrayPool; import org.exist.util.ByteConversion; import org.exist.util.UTF8; import org.exist.util.pool.NodePool; import org.exist.xmldb.XmldbURI; import org.exist.xquery.Constants; import org.exist.xquery.value.StringValue; import org.w3c.dom.Attr; import org.w3c.dom.CDATASection; import org.w3c.dom.Comment; import org.w3c.dom.DOMException; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.ProcessingInstruction; import org.w3c.dom.Text; import org.w3c.dom.TypeInfo; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.InputStream; import java.io.IOException; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import javax.xml.XMLConstants; import javax.xml.stream.XMLStreamConstants; import javax.xml.stream.XMLStreamException; /** * ElementImpl.java * * @author Wolfgang Meier */ public class ElementImpl extends NamedNode implements Element { public static final int LENGTH_ELEMENT_CHILD_COUNT = 4; //sizeof int public static final int LENGTH_ATTRIBUTES_COUNT = 2; //sizeof short public static final int LENGTH_NS_ID = 2; //sizeof short public static final int LENGTH_PREFIX_LENGTH = 2; //sizeof short private short attributes = 0; private int children = 0; private int position = 0; private Map<String, String> namespaceMappings = null; private int indexType = RangeIndexSpec.NO_INDEX; private boolean preserveWS = false; private boolean isDirty = false; public ElementImpl() { super(Node.ELEMENT_NODE); } /** * Constructor for the ElementImpl object * * @param nodeName Description of the Parameter */ public ElementImpl(final QName nodeName, final SymbolTable symbols) throws DOMException { super(Node.ELEMENT_NODE, nodeName); this.nodeName = nodeName; if(symbols.getSymbol(nodeName.getLocalPart()) < 0) { throw new DOMException(DOMException.INVALID_ACCESS_ERR, "Too many element/attribute names registered in the database. No of distinct names is limited to 16bit. Aborting store."); } } public ElementImpl(final ElementImpl other) { super(other); this.children = other.children; this.attributes = other.attributes; this.namespaceMappings = other.namespaceMappings; this.indexType = other.indexType; this.position = other.position; } /** * Reset this element to its initial state. */ @Override public void clear() { super.clear(); attributes = 0; children = 0; position = 0; namespaceMappings = null; //TODO : reset below as well ? -pb //indexType //preserveWS } public void setIndexType(final int idxType) { this.indexType = idxType; } public int getIndexType() { return indexType; } @Override public boolean isDirty() { return isDirty; } @Override public void setDirty(final boolean dirty) { this.isDirty = dirty; } public void setPosition(final int position) { this.position = position; } public int getPosition() { return position; } public boolean declaresNamespacePrefixes() { if(namespaceMappings == null) { return false; } return namespaceMappings.size() > 0; } /** * Serializes a (persistent DOM) Element to a byte array * * data = signature childCount nodeIdUnitsLength nodeId attributesCount localNameId namespace? prefixData? * * signature = [byte] 0x20 | localNameType | hasNamespace? | isDirty? * * localNameType = noContent OR intContent OR shortContent OR byteContent * noContent = 0x0 * intContent = 0x1 * shortContent = 0x2 * byteContent = 0x3 * * hasNamespace = 0x10 * * isDirty = 0x8 * * childCount = [int] (4 bytes) The number of child nodes * * nodeIdUnitsLength = [short] (2 bytes) The number of units of the element's NodeId * nodeId = @see org.exist.numbering.DLNBase#serialize(byte[], int) * * attributesCount = [short] (2 bytes) The number of attributes * * localNameId = [int] (4 bytes) | [short] (2 bytes) | [byte] 1 byte. The Id of the element's local name from SymbolTable (symbols.dbx) * * namespace = namespaceUriId namespacePrefixLength elementNamespacePrefix? * namespaceUriId = [short] (2 bytes) The Id of the namespace URI from SymbolTable (symbols.dbx) * namespacePrefixLength = [short] (2 bytes) * elementNamespacePrefix = eUtf8 * * eUtf8 = {@see org.exist.util.UTF8#encode(java.lang.String, byte[], int)} * * prefixData = namespaceMappingsCount namespaceMapping+ * namespaceMappingsCount = [short] (2 bytes) * namespaceMapping = namespacePrefix namespaceUriId * namespacePrefix = jUtf8 * * jUtf8 = {@see java.io.DataOutputStream#writeUTF(java.lang.String)} */ @Override public byte[] serialize() { if(nodeId == null) { throw new RuntimeException("nodeId = null for element: " + getQName().getStringValue()); } try { final SymbolTable symbols = ownerDocument.getBrokerPool().getSymbols(); byte[] prefixData = null; // serialize namespace prefixes declared in this element if(declaresNamespacePrefixes()) { final ByteArrayOutputStream bout = new ByteArrayOutputStream(); final DataOutputStream out = new DataOutputStream(bout); out.writeShort(namespaceMappings.size()); for(final Iterator<Map.Entry<String, String>> i = namespaceMappings.entrySet().iterator(); i.hasNext(); ) { final Map.Entry<String, String> entry = i.next(); out.writeUTF(entry.getKey()); final short nsId = symbols.getNSSymbol(entry.getValue()); out.writeShort(nsId); } prefixData = bout.toByteArray(); } final short id = symbols.getSymbol(this); final boolean hasNamespace = nodeName.hasNamespace(); short nsId = 0; if(hasNamespace) { nsId = symbols.getNSSymbol(nodeName.getNamespaceURI()); } final byte idSizeType = Signatures.getSizeType(id); byte signature = (byte) ((Signatures.Elem << 0x5) | idSizeType); int prefixLen = 0; if(hasNamespace) { if(nodeName.getPrefix() != null && nodeName.getPrefix().length() > 0) { prefixLen = UTF8.encoded(nodeName.getPrefix()); } signature |= 0x10; } if(isDirty) { signature |= 0x8; } final int nodeIdLen = nodeId.size(); final byte[] data = ByteArrayPool.getByteArray( StoredNode.LENGTH_SIGNATURE_LENGTH + LENGTH_ELEMENT_CHILD_COUNT + NodeId.LENGTH_NODE_ID_UNITS + nodeIdLen + LENGTH_ATTRIBUTES_COUNT + Signatures.getLength(idSizeType) + (hasNamespace ? prefixLen + 4 : 0) + (prefixData != null ? prefixData.length : 0) ); int next = 0; data[next] = signature; next += StoredNode.LENGTH_SIGNATURE_LENGTH; ByteConversion.intToByte(children, data, next); next += LENGTH_ELEMENT_CHILD_COUNT; ByteConversion.shortToByte((short) nodeId.units(), data, next); next += NodeId.LENGTH_NODE_ID_UNITS; nodeId.serialize(data, next); next += nodeIdLen; ByteConversion.shortToByte(attributes, data, next); next += LENGTH_ATTRIBUTES_COUNT; Signatures.write(idSizeType, id, data, next); next += Signatures.getLength(idSizeType); if(hasNamespace) { ByteConversion.shortToByte(nsId, data, next); next += LENGTH_NS_ID; ByteConversion.shortToByte((short) prefixLen, data, next); next += LENGTH_PREFIX_LENGTH; if(nodeName.getPrefix() != null && nodeName.getPrefix().length() > 0) { UTF8.encode(nodeName.getPrefix(), data, next); } next += prefixLen; } if(prefixData != null) { System.arraycopy(prefixData, 0, data, next, prefixData.length); } return data; } catch(final IOException e) { LOG.error(e); return null; } } public static StoredNode deserialize(final byte[] data, final int start, final int len, final DocumentImpl doc, final boolean pooled) { final int end = start + len; int pos = start; final byte idSizeType = (byte) (data[pos] & 0x03); boolean isDirty = (data[pos] & 0x8) == 0x8; final boolean hasNamespace = (data[pos] & 0x10) == 0x10; pos += StoredNode.LENGTH_SIGNATURE_LENGTH; final int children = ByteConversion.byteToInt(data, pos); pos += LENGTH_ELEMENT_CHILD_COUNT; final int dlnLen = ByteConversion.byteToShort(data, pos); pos += NodeId.LENGTH_NODE_ID_UNITS; final NodeId dln = doc.getBrokerPool().getNodeFactory().createFromData(dlnLen, data, pos); pos += dln.size(); final short attributes = ByteConversion.byteToShort(data, pos); pos += LENGTH_ATTRIBUTES_COUNT; final short id = (short) Signatures.read(idSizeType, data, pos); pos += Signatures.getLength(idSizeType); short nsId = 0; String prefix = null; if(hasNamespace) { nsId = ByteConversion.byteToShort(data, pos); pos += LENGTH_NS_ID; int prefixLen = ByteConversion.byteToShort(data, pos); pos += LENGTH_PREFIX_LENGTH; if(prefixLen > 0) { prefix = UTF8.decode(data, pos, prefixLen).toString(); } pos += prefixLen; } final String name = doc.getBrokerPool().getSymbols().getName(id); String namespace = XMLConstants.NULL_NS_URI; if(nsId != 0) { namespace = doc.getBrokerPool().getSymbols().getNamespace(nsId); } final ElementImpl node; if(pooled) { node = (ElementImpl) NodePool.getInstance().borrowNode(Node.ELEMENT_NODE); } else { node = new ElementImpl(); } node.setNodeId(dln); node.nodeName = doc.getBrokerPool().getSymbols().getQName(Node.ELEMENT_NODE, namespace, name, prefix); node.children = children; node.attributes = attributes; node.isDirty = isDirty; node.setOwnerDocument(doc); //TO UNDERSTAND : why is this code here ? if(end > pos) { final byte[] pfxData = new byte[end - pos]; System.arraycopy(data, pos, pfxData, 0, end - pos); final InputStream bin = new ByteArrayInputStream(pfxData); final DataInputStream in = new DataInputStream(bin); try { final short prefixCount = in.readShort(); for(int i = 0; i < prefixCount; i++) { prefix = in.readUTF(); nsId = in.readShort(); node.addNamespaceMapping(prefix, doc.getBrokerPool().getSymbols().getNamespace(nsId)); } } catch(final IOException e) { LOG.error(e); } } return node; } public static QName readQName(final Value value, final DocumentImpl document, final NodeId nodeId) { final byte[] data = value.data(); int offset = value.start(); final byte idSizeType = (byte) (data[offset] & 0x03); final boolean hasNamespace = (data[offset] & 0x10) == 0x10; offset += StoredNode.LENGTH_SIGNATURE_LENGTH; offset += LENGTH_ELEMENT_CHILD_COUNT; offset += NodeId.LENGTH_NODE_ID_UNITS; offset += nodeId.size(); offset += LENGTH_ATTRIBUTES_COUNT; final short id = (short) Signatures.read(idSizeType, data, offset); offset += Signatures.getLength(idSizeType); short nsId = 0; String prefix = null; if(hasNamespace) { nsId = ByteConversion.byteToShort(data, offset); offset += LENGTH_NS_ID; int prefixLen = ByteConversion.byteToShort(data, offset); offset += LENGTH_PREFIX_LENGTH; if(prefixLen > 0) { prefix = UTF8.decode(data, offset, prefixLen).toString(); } offset += prefixLen; } final String name = document.getBrokerPool().getSymbols().getName(id); final String namespace; if(nsId != 0) { namespace = document.getBrokerPool().getSymbols().getNamespace(nsId); } else { namespace = XMLConstants.NULL_NS_URI; } return new QName(name, namespace, prefix == null ? XMLConstants.DEFAULT_NS_PREFIX : prefix); } public static void readNamespaceDecls(final List<String[]> namespaces, final Value value, final DocumentImpl document, final NodeId nodeId) { final byte[] data = value.data(); int offset = value.start(); final int end = offset + value.getLength(); final byte idSizeType = (byte) (data[offset] & 0x03); final boolean hasNamespace = (data[offset] & 0x10) == 0x10; offset += StoredNode.LENGTH_SIGNATURE_LENGTH; offset += LENGTH_ELEMENT_CHILD_COUNT; offset += NodeId.LENGTH_NODE_ID_UNITS; offset += nodeId.size(); offset += LENGTH_ATTRIBUTES_COUNT; offset += Signatures.getLength(idSizeType); if(hasNamespace) { offset += LENGTH_NS_ID; int prefixLen = ByteConversion.byteToShort(data, offset); offset += LENGTH_PREFIX_LENGTH; offset += prefixLen; } if(end > offset) { final byte[] pfxData = new byte[end - offset]; System.arraycopy(data, offset, pfxData, 0, end - offset); final InputStream bin = new ByteArrayInputStream(pfxData); final DataInputStream in = new DataInputStream(bin); try { final short prefixCount = in.readShort(); String prefix; short nsId; for(int i = 0; i < prefixCount; i++) { prefix = in.readUTF(); nsId = in.readShort(); namespaces.add(new String[]{prefix, document.getBrokerPool().getSymbols().getNamespace(nsId)}); } } catch(final IOException e) { LOG.error(e); } } } public void addNamespaceMapping(final String prefix, final String ns) { if(prefix == null) { return; } if(namespaceMappings == null) { namespaceMappings = new HashMap<>(1); } else if(namespaceMappings.containsKey(prefix)) { return; } namespaceMappings.put(prefix, ns); } /** * Append a child to this node. This method does not rearrange the * node tree and is only used internally by the parser. * * @param child * @throws DOMException */ public void appendChildInternal(final IStoredNode prevNode, final NodeHandle child) throws DOMException { final NodeId childId; if(prevNode == null) { childId = getNodeId().newChild(); } else { if(prevNode.getNodeId() == null) { LOG.warn(getQName() + " : " + prevNode.getNodeName()); } childId = prevNode.getNodeId().nextSibling(); } child.setNodeId(childId); ++children; } @Override public Node appendChild(final Node child) throws DOMException { final TransactionManager transact = ownerDocument.getBrokerPool().getTransactionManager(); final org.exist.dom.NodeListImpl nl = new org.exist.dom.NodeListImpl(); nl.add(child); try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker(); final Txn transaction = transact.beginTransaction()) { appendChildren(transaction, nl, 0); broker.storeXMLResource(transaction, getOwnerDocument()); transact.commit(transaction); // bugID 3419602 return getLastChild(); } catch(final Exception e) { throw new DOMException(DOMException.INVALID_STATE_ERR, e.getMessage()); } } private void appendAttributes(final Txn transaction, final NodeList attribs) throws DOMException { final NodeList duplicateAttrs = findDupAttributes(attribs); removeAppendAttributes(transaction, duplicateAttrs, attribs); } private NodeList checkForAttributes(final Txn transaction, final NodeList nodes) throws DOMException { org.exist.dom.NodeListImpl attribs = null; org.exist.dom.NodeListImpl rest = null; for(int i = 0; i < nodes.getLength(); i++) { final Node next = nodes.item(i); if(next.getNodeType() == Node.ATTRIBUTE_NODE) { if(!next.getNodeName().startsWith(XMLConstants.XMLNS_ATTRIBUTE)) { if(attribs == null) { attribs = new org.exist.dom.NodeListImpl(); } attribs.add(next); } } else if(attribs != null) { if(rest == null) { rest = new org.exist.dom.NodeListImpl(); } rest.add(next); } } if(attribs == null) { return nodes; } appendAttributes(transaction, attribs); return rest; } @Override public void appendChildren(final Txn transaction, NodeList nodes, final int child) throws DOMException { // attributes are handled differently. Call checkForAttributes to extract them. nodes = checkForAttributes(transaction, nodes); if(nodes == null || nodes.getLength() == 0) { return; } try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker()) { final NodePath path = getPath(); StreamListener listener = null; final IndexController indexes = broker.getIndexController(); //May help getReindexRoot() to make some useful things indexes.setDocument(ownerDocument); final IStoredNode reindexRoot = indexes.getReindexRoot(this, path, true, true); indexes.setMode(ReindexMode.STORE); // only reindex if reindexRoot is an ancestor of the current node if(reindexRoot == null) { listener = indexes.getStreamListener(); } if(children == 0) { // no children: append a new child appendChildren(transaction, nodeId.newChild(), null, new NodeImplRef(this), path, nodes, listener); } else { if(child == 1) { final Node firstChild = getFirstChild(); insertBefore(transaction, nodes, firstChild); } else { if(child > 1 && child <= children) { final NodeList cl = getChildNodes(); final IStoredNode<?> last = (IStoredNode<?>) cl.item(child - 2); insertAfter(transaction, nodes, last); } else { final IStoredNode<?> last = (IStoredNode<?>) getLastChild(); appendChildren(transaction, last.getNodeId().nextSibling(), null, new NodeImplRef(getLastNode(last)), path, nodes, listener); } } } broker.updateNode(transaction, this, false); indexes.reindex(transaction, reindexRoot, ReindexMode.STORE); broker.flush(); } catch(final EXistException e) { LOG.warn("Exception while appending child node: " + e.getMessage(), e); } } /** * Internal append. * * @throws DOMException */ private void appendChildren(final Txn transaction, NodeId newNodeId, final NodeId followingId, final NodeImplRef last, final NodePath lastPath, final NodeList nodes, final StreamListener listener) throws DOMException { if(last == null || last.getNode() == null || last.getNode().getOwnerDocument() == null) { throw new DOMException(DOMException.INVALID_MODIFICATION_ERR, "invalid node"); } children += nodes.getLength(); for(int i = 0; i < nodes.getLength(); i++) { final Node child = nodes.item(i); appendChild(transaction, newNodeId, last, lastPath, child, listener); NodeId next = newNodeId.nextSibling(); if(followingId != null && next.equals(followingId)) { next = newNodeId.insertNode(followingId); if(LOG.isDebugEnabled()) { LOG.debug("Node ID collision on " + followingId + ". Using " + next + " instead."); } } newNodeId = next; } } private Node appendChild(final Txn transaction, final NodeId newNodeId, final NodeImplRef last, final NodePath lastPath, final Node child, final StreamListener listener) throws DOMException { if(last == null || last.getNode() == null) { throw new DOMException(DOMException.INVALID_MODIFICATION_ERR, "invalid node"); } final DocumentImpl owner = getOwnerDocument(); try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker()) { switch(child.getNodeType()) { case Node.DOCUMENT_FRAGMENT_NODE: appendChildren(transaction, newNodeId, null, last, lastPath, child.getChildNodes(), listener); return null; // TODO: implement document fragments so //we can return all newly appended children case Node.ELEMENT_NODE: // create new element final ElementImpl elem = new ElementImpl( new QName(child.getLocalName() == null ? child.getNodeName() : child.getLocalName(), child.getNamespaceURI(), child.getPrefix()), broker.getBrokerPool().getSymbols() ); elem.setNodeId(newNodeId); elem.setOwnerDocument(owner); final org.exist.dom.NodeListImpl ch = new org.exist.dom.NodeListImpl(); final NamedNodeMap attribs = child.getAttributes(); int numActualAttribs = 0; for(int i = 0; i < attribs.getLength(); i++) { final Attr attr = (Attr) attribs.item(i); if(!attr.getNodeName().startsWith(XMLConstants.XMLNS_ATTRIBUTE)) { ch.add(attr); numActualAttribs++; } else { final String xmlnsDecl = attr.getNodeName(); final String prefix = xmlnsDecl.length() == 5 ? XMLConstants.DEFAULT_NS_PREFIX : xmlnsDecl.substring(6); elem.addNamespaceMapping(prefix, attr.getNodeValue()); } } final NodeList cl = child.getChildNodes(); for(int i = 0; i < cl.getLength(); i++) { final Node n = cl.item(i); if(n.getNodeType() != Node.ATTRIBUTE_NODE) { ch.add(n); } } elem.setChildCount(ch.getLength()); if(numActualAttribs != (short) numActualAttribs) { throw new DOMException(DOMException.INVALID_MODIFICATION_ERR, "Too many attributes"); } elem.setAttributes((short) numActualAttribs); lastPath.addComponent(elem.getQName()); // insert the node broker.insertNodeAfter(transaction, last.getNode(), elem); broker.indexNode(transaction, elem, lastPath); final IndexController indexes = broker.getIndexController(); indexes.startIndexDocument(transaction, listener); try { indexes.indexNode(transaction, elem, lastPath, listener); elem.setChildCount(0); last.setNode(elem); //process child nodes elem.appendChildren(transaction, newNodeId.newChild(), null, last, lastPath, ch, listener); broker.endElement(elem, lastPath, null); indexes.endElement(transaction, elem, lastPath, listener); } finally { indexes.endIndexDocument(transaction, listener); } lastPath.removeLastComponent(); return elem; case Node.TEXT_NODE: final TextImpl text = new TextImpl(newNodeId, ((Text)child).getData()); text.setOwnerDocument(owner); // insert the node broker.insertNodeAfter(transaction, last.getNode(), text); broker.indexNode(transaction, text, lastPath); broker.getIndexController().indexNode(transaction, text, lastPath, listener); last.setNode(text); return text; case Node.CDATA_SECTION_NODE: final CDATASectionImpl cdata = new CDATASectionImpl(newNodeId, ((CDATASection)child).getData()); cdata.setOwnerDocument(owner); // insert the node broker.insertNodeAfter(transaction, last.getNode(), cdata); broker.indexNode(transaction, cdata, lastPath); last.setNode(cdata); return cdata; case Node.ATTRIBUTE_NODE: final Attr attr = (Attr) child; final String ns = attr.getNamespaceURI(); final String prefix = (Namespaces.XML_NS.equals(ns) ? XMLConstants.XML_NS_PREFIX : attr.getPrefix()); String name = attr.getLocalName(); if(name == null) { name = attr.getName(); } final QName attrName = new QName(name, ns, prefix); final AttrImpl attrib = new AttrImpl(attrName, attr.getValue(), broker.getBrokerPool().getSymbols()); attrib.setNodeId(newNodeId); attrib.setOwnerDocument(owner); if(ns != null && attrName.compareTo(Namespaces.XML_ID_QNAME) == Constants.EQUAL) { // an xml:id attribute. Normalize the attribute and set its type to ID attrib.setValue(StringValue.trimWhitespace(StringValue.collapseWhitespace(attrib.getValue()))); attrib.setType(AttrImpl.ID); } else { attrib.setQName(new QName(attrib.getQName(), ElementValue.ATTRIBUTE)); } broker.insertNodeAfter(transaction, last.getNode(), attrib); broker.indexNode(transaction, attrib, lastPath); broker.getIndexController().indexNode(transaction, attrib, lastPath, listener); last.setNode(attrib); return attrib; case Node.COMMENT_NODE: final CommentImpl comment = new CommentImpl(((Comment)child).getData()); comment.setNodeId(newNodeId); comment.setOwnerDocument(owner); // insert the node broker.insertNodeAfter(transaction, last.getNode(), comment); broker.indexNode(transaction, comment, lastPath); last.setNode(comment); return comment; case Node.PROCESSING_INSTRUCTION_NODE: final ProcessingInstructionImpl pi = new ProcessingInstructionImpl(newNodeId, ((ProcessingInstruction)child).getTarget(), ((ProcessingInstruction)child).getData()); pi.setOwnerDocument(owner); //insert the node broker.insertNodeAfter(transaction, last.getNode(), pi); broker.indexNode(transaction, pi, lastPath); last.setNode(pi); return pi; default: throw new DOMException(DOMException.INVALID_MODIFICATION_ERR, "Unknown node type: " + child.getNodeType() + " " + child.getNodeName()); } } catch(final EXistException e) { LOG.warn("Exception while appending node: " + e.getMessage(), e); } return null; } @Override public boolean hasAttributes() { return attributes > 0; } public void setAttributes(final short attribNum) { attributes = attribNum; } @Override public String getAttribute(final String name) { final Attr attr = findAttribute(name); return attr != null ? attr.getValue() : ""; } @Override public String getAttributeNS(final String namespaceURI, final String localName) { final Attr attr = findAttribute(new QName(localName, namespaceURI)); return attr != null ? attr.getValue() : XMLConstants.NULL_NS_URI; //XXX: if not present must return null } @Deprecated //move as soon as getAttributeNS null issue resolved public String _getAttributeNS(final String namespaceURI, final String localName) { final Attr attr = findAttribute(new QName(localName, namespaceURI)); return attr != null ? attr.getValue() : null; } @Override public Attr getAttributeNode(final String name) { return findAttribute(name); } @Override public Attr getAttributeNodeNS(final String namespaceURI, final String localName) { return findAttribute(new QName(localName, namespaceURI)); } @Override public NamedNodeMap getAttributes() { final org.exist.dom.NamedNodeMapImpl map = new NamedNodeMapImpl(); if(hasAttributes()) { try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker(); final INodeIterator iterator = broker.getNodeIterator(this)) { iterator.next(); final int childCount = getChildCount(); for(int i = 0; i < childCount; i++) { final IStoredNode next = iterator.next(); if(next.getNodeType() != Node.ATTRIBUTE_NODE) { break; } map.setNamedItem(next); } } catch(final EXistException | IOException e) { LOG.warn("Exception while retrieving attributes: " + e.getMessage()); } } if(declaresNamespacePrefixes()) { for(final Iterator<Map.Entry<String, String>> i = namespaceMappings.entrySet().iterator(); i.hasNext(); ) { final Map.Entry<String, String> entry = i.next(); final String prefix = entry.getKey(); final String ns = entry.getValue(); final QName attrName = new QName(prefix, Namespaces.XMLNS_NS, XMLConstants.XMLNS_ATTRIBUTE); final AttrImpl attr = new AttrImpl(attrName, ns, null); attr.setOwnerDocument(ownerDocument); map.setNamedItem(attr); } } return map; } private AttrImpl findAttribute(final String qname) { try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker(); final INodeIterator iterator = broker.getNodeIterator(this)) { iterator.next(); return findAttribute(qname, iterator, this); } catch(final EXistException | IOException e) { LOG.warn("Exception while retrieving attributes: " + e.getMessage()); } return null; } private AttrImpl findAttribute(final String qname, final INodeIterator iterator, final IStoredNode current) { final int childCount = current.getChildCount(); IStoredNode next; for(int i = 0; i < childCount; i++) { next = iterator.next(); if(next.getNodeType() != Node.ATTRIBUTE_NODE) { break; } if(next.getNodeName().equals(qname)) { return (AttrImpl) next; } } return null; } private AttrImpl findAttribute(final QName qname) { try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker(); final INodeIterator iterator = broker.getNodeIterator(this)) { iterator.next(); return findAttribute(qname, iterator, this); } catch(final EXistException | IOException e) { LOG.warn("Exception while retrieving attributes: " + e.getMessage()); } return null; } private AttrImpl findAttribute(final QName qname, final INodeIterator iterator, final IStoredNode current) { final int childCount = current.getChildCount(); for(int i = 0; i < childCount; i++) { final IStoredNode next = iterator.next(); if(next.getNodeType() != Node.ATTRIBUTE_NODE) { break; } if(next.getQName().equals(qname)) { return (AttrImpl) next; } } return null; } /** * Returns a list of all attribute nodes in attrs that are already present * in the current element. * * @param attrs * @return The attributes list * @throws DOMException */ private NodeList findDupAttributes(final NodeList attrs) throws DOMException { org.exist.dom.NodeListImpl dupList = null; final NamedNodeMap map = getAttributes(); for(int i = 0; i < attrs.getLength(); i++) { final Node attr = attrs.item(i); //Workaround: Xerces sometimes returns null for getLocalPart() !!!! String localName = attr.getLocalName(); if(localName == null) { localName = attr.getNodeName(); } final Node duplicate = map.getNamedItemNS(attr.getNamespaceURI(), localName); if(duplicate != null) { if(dupList == null) { dupList = new org.exist.dom.NodeListImpl(); } dupList.add(duplicate); } } return dupList; } @Override public int getChildCount() { return children; } @Override public NodeList getChildNodes() { final org.exist.dom.NodeListImpl childList = new org.exist.dom.NodeListImpl(1); try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker()) { for(final IEmbeddedXMLStreamReader reader = broker.getXMLStreamReader(this, true); reader.hasNext(); ) { final int status = reader.next(); if(status != XMLStreamConstants.END_ELEMENT && ((NodeId) reader.getProperty(ExtendedXMLStreamReader.PROPERTY_NODE_ID)).isChildOf(nodeId)) { childList.add(reader.getNode()); } } } catch(final IOException | XMLStreamException | EXistException e) { LOG.warn("Internal error while reading child nodes: " + e.getMessage(), e); } return childList; } @Override public NodeList getElementsByTagName(final String tagName) { final QName qname = new QName(tagName); return getOwnerDocument().findElementsByTagName(this, qname); } @Override public NodeList getElementsByTagNameNS(final String namespaceURI, final String localName) { final QName qname = new QName(localName, namespaceURI); return getOwnerDocument().findElementsByTagName(this, qname); } @Override public Node getFirstChild() { if(!hasChildNodes() || getChildCount() == attributes) { return null; } try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker(); final INodeIterator iterator = broker.getNodeIterator(this)) { iterator.next(); IStoredNode next; for(int i = 0; i < getChildCount(); i++) { next = iterator.next(); if(next.getNodeType() != Node.ATTRIBUTE_NODE) { return next; } } } catch(final EXistException | IOException e) { LOG.warn("Exception while retrieving child node: " + e.getMessage(), e); } return null; } @Override public Node getLastChild() { if(!hasChildNodes()) { return null; } Node node = null; if(!isDirty) { final NodeId child = nodeId.getChild(children); node = ownerDocument.getNode(new NodeProxy(ownerDocument, child)); } if(node == null) { final NodeList cl = getChildNodes(); return cl.item(cl.getLength() - 1); } return node; } @Override public String getTagName() { return nodeName.getStringValue(); } @Override public boolean hasAttribute(final String name) { return findAttribute(name) != null; } @Override public boolean hasAttributeNS(final String namespaceURI, final String localName) { return findAttribute(new QName(localName, namespaceURI)) != null; } /** * @see org.w3c.dom.Node#getNodeValue() */ //TODO getNodeValue() on org.exist.dom.persistent.ElementImpl should return null according to W3C spec, and getTextContent() should be implemented! @Override public String getNodeValue() throws DOMException { //TODO : parametrize the boolean value ? try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker()) { return broker.getNodeValue(this, false); } catch(final EXistException e) { LOG.warn("Exception while reading node value: " + e.getMessage(), e); } return ""; } /** * @see org.w3c.dom.Element#removeAttribute(java.lang.String) */ @Override public void removeAttribute(final String name) throws DOMException { throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "removeAttribute(String name) not implemented on class " + getClass().getName()); } /** * @see org.w3c.dom.Element#removeAttributeNS(java.lang.String, java.lang.String) */ @Override public void removeAttributeNS(final String namespaceURI, final String name) throws DOMException { throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "removeAttributeNS(String namespaceURI, String name) not implemented on class " + getClass().getName()); } @Override public Attr removeAttributeNode(final Attr oldAttr) throws DOMException { throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "removeAttributeNode(Attr oldAttr) not implemented on class " + getClass().getName()); } @Override public void setAttribute(final String name, final String value) throws DOMException { throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "setAttribute(String name, String value) not implemented on class " + getClass().getName()); } @Override public void setAttributeNS(final String namespaceURI, final String qualifiedName, final String value) throws DOMException { throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "setAttributeNS(String namespaceURI, String qualifiedName," + "String value) not implemented on class " + getClass().getName()); } @Override public Attr setAttributeNode(final Attr newAttr) throws DOMException { throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "setAttributeNode(Attr newAttr) not implemented on class " + getClass().getName()); } @Override public Attr setAttributeNodeNS(final Attr newAttr) { throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "setAttributeNodeNS(Attr newAttr) not implemented on class " + getClass().getName()); } public void setChildCount(final int count) { children = count; } public void setNamespaceMappings(final Map<String, String> map) { this.namespaceMappings = new HashMap<>(map); for(final String ns : namespaceMappings.values()) { ownerDocument.getBrokerPool().getSymbols().getNSSymbol(ns); } } public Iterator<String> getPrefixes() { return namespaceMappings.keySet().iterator(); } public String getNamespaceForPrefix(final String prefix) { return namespaceMappings.get(prefix); } /** * @see java.lang.Object#toString() */ @Override public String toString() { return toString(true); } /** */ @Override public String toString(final boolean top) { return toString(top, new TreeSet<String>()); } /** * Method toString. */ private String toString(final boolean top, final Set<String> namespaces) { final StringBuilder buf = new StringBuilder(); final StringBuilder attributes = new StringBuilder(); final StringBuilder children = new StringBuilder(); buf.append('<'); buf.append(nodeName); //Remove false to have a verbose output //if (top && false) { //buf.append(" xmlns:exist=\""+ Namespaces.EXIST_NS + "\""); //buf.append(" exist:id=\""); //buf.append(getNodeId()); //buf.append("\" exist:document=\""); //buf.append(((DocumentImpl)getOwnerDocument()).getFileURI()); //buf.append("\""); //} if(declaresNamespacePrefixes()) { // declare namespaces used by this element Map.Entry<String, String> entry; String namespace, prefix; for(final Iterator<Map.Entry<String, String>> i = namespaceMappings.entrySet().iterator(); i.hasNext(); ) { entry = i.next(); prefix = entry.getKey(); namespace = entry.getValue(); buf.append(" ").append(XMLConstants.XMLNS_ATTRIBUTE); if(prefix.length() == 0) { buf.append("\""); //.append(namespace); } else { buf.append(":") .append(prefix) .append("=\""); //.append(namespace); } buf.append("...\" "); namespaces.add(namespace); } } if(nodeName.getNamespaceURI().length() > 0 && (!namespaces.contains(nodeName.getNamespaceURI()))) { buf.append(" ") .append(XMLConstants.XMLNS_ATTRIBUTE) .append(":") .append(nodeName.getPrefix()).append("=\"") .append(nodeName.getNamespaceURI()) .append("\" "); } final NodeList childNodes = getChildNodes(); for(int i = 0; i < childNodes.getLength(); i++) { final Node child = childNodes.item(i); switch(child.getNodeType()) { case Node.ATTRIBUTE_NODE: attributes.append(' ') .append(((Attr) child).getName()) .append("=\"") .append(escapeXml(child)) .append("\""); break; case Node.ELEMENT_NODE: children.append(((ElementImpl) child).toString(false, namespaces)); break; default: children.append(child.toString()); } } if(attributes.length() > 0) { buf.append(attributes.toString()); } if(childNodes.getLength() > 0) { buf.append(">"); buf.append(children.toString()); buf.append("</"); buf.append(nodeName); buf.append(">"); } else { buf.append("/>"); } return buf.toString(); } @Override public Node insertBefore(final Node newChild, final Node refChild) throws DOMException { if(refChild == null) { return appendChild(newChild); } else if(!(refChild instanceof IStoredNode)) { throw new DOMException(DOMException.WRONG_DOCUMENT_ERR, "Wrong node type"); } final org.exist.dom.NodeListImpl nl = new org.exist.dom.NodeListImpl(); nl.add(newChild); final TransactionManager transact = ownerDocument.getBrokerPool().getTransactionManager(); try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker(); final Txn transaction = transact.beginTransaction()) { insertBefore(transaction, nl, refChild); broker.storeXMLResource(transaction, getOwnerDocument()); transact.commit(transaction); return refChild.getPreviousSibling(); } catch(final TransactionException e) { throw new DOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR, e.getMessage()); } catch(final EXistException e) { LOG.warn("Exception while inserting node: " + e.getMessage(), e); } return null; } /** * Insert a list of nodes at the position before the reference * child. */ @Override public void insertBefore(final Txn transaction, final NodeList nodes, final Node refChild) throws DOMException { if(refChild == null) { //TODO : use NodeImpl.UNKNOWN_NODE_IMPL_GID ? -pb appendChildren(transaction, nodes, -1); return; } else if(!(refChild instanceof IStoredNode)) { throw new DOMException(DOMException.WRONG_DOCUMENT_ERR, "wrong node type"); } try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker()) { final NodePath path = getPath(); final IndexController indexes = broker.getIndexController(); //May help getReindexRoot() to make some useful things indexes.setDocument(ownerDocument); final IStoredNode reindexRoot = indexes.getReindexRoot(this, path, true, true); indexes.setMode(ReindexMode.STORE); final StreamListener listener; if(reindexRoot == null) { listener = indexes.getStreamListener(); } else { listener = null; } final IStoredNode<?> following = (IStoredNode<?>) refChild; final IStoredNode<?> previous = (IStoredNode<?>) following.getPreviousSibling(); if(previous == null) { // there's no sibling node before the new node final NodeId newId = following.getNodeId().insertBefore(); appendChildren(transaction, newId, following.getNodeId(), new NodeImplRef(this), path, nodes, listener); } else { // insert the new node between the preceding and following sibling final NodeId newId = previous.getNodeId().insertNode(following.getNodeId()); appendChildren(transaction, newId, following.getNodeId(), new NodeImplRef(getLastNode(previous)), path, nodes, listener); } setDirty(true); broker.updateNode(transaction, this, true); indexes.reindex(transaction, reindexRoot, ReindexMode.STORE); broker.flush(); } catch(final EXistException e) { LOG.warn("Exception while inserting node: " + e.getMessage(), e); } } /** * Insert a list of nodes at the position following the reference * child. */ @Override public void insertAfter(final Txn transaction, final NodeList nodes, final Node refChild) throws DOMException { if(refChild == null) { //TODO : use NodeImpl.UNKNOWN_NODE_IMPL_GID ? -pb appendChildren(null, nodes, -1); return; } else if(!(refChild instanceof IStoredNode)) { throw new DOMException(DOMException.WRONG_DOCUMENT_ERR, "wrong node type: "); } try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker()) { final NodePath path = getPath(); final IndexController indexes = broker.getIndexController(); //May help getReindexRoot() to make some useful things indexes.setDocument(ownerDocument); final IStoredNode reindexRoot = indexes.getReindexRoot(this, path, true, true); indexes.setMode(ReindexMode.STORE); final StreamListener listener; if(reindexRoot == null) { listener = indexes.getStreamListener(); } else { listener = null; } final IStoredNode<?> previous = (IStoredNode<?>) refChild; final IStoredNode<?> following = (IStoredNode<?>) previous.getNextSibling(); final NodeId followingId = following == null ? null : following.getNodeId(); final NodeId newNodeId = previous.getNodeId().insertNode(followingId); appendChildren(transaction, newNodeId, followingId, new NodeImplRef(getLastNode(previous)), path, nodes, listener); setDirty(true); broker.updateNode(transaction, this, true); indexes.reindex(transaction, reindexRoot, ReindexMode.STORE); broker.flush(); } catch(final EXistException e) { LOG.warn("Exception while inserting node: " + e.getMessage(), e); } } /** * Update the contents of this element. The passed list of nodes * becomes the new content. * * @param newContent * @throws DOMException */ public void update(final Txn transaction, final NodeList newContent) throws DOMException { final NodePath path = getPath(); // remove old child nodes final NodeList nodes = getChildNodes(); try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker()) { final IndexController indexes = broker.getIndexController(); //May help getReindexRoot() to make some useful things indexes.setDocument(ownerDocument); final IStoredNode reindexRoot = indexes.getReindexRoot(this, path, true, true); indexes.setMode(ReindexMode.REMOVE_SOME_NODES); final StreamListener listener; if(reindexRoot == null) { listener = indexes.getStreamListener(); } else { listener = null; indexes.reindex(transaction, reindexRoot, ReindexMode.REMOVE_SOME_NODES); } // TODO: fix once range index has been moved to new architecture final IStoredNode valueReindexRoot = broker.getValueIndex().getReindexRoot(this, path); broker.getValueIndex().reindex(valueReindexRoot); IStoredNode last = this; int i = nodes.getLength(); for(; i > 0; i--) { IStoredNode<?> child = (IStoredNode<?>) nodes.item(i - 1); if(child.getNodeType() == Node.ATTRIBUTE_NODE) { last = child; break; } if(child.getNodeType() == Node.ELEMENT_NODE) { path.addComponent(child.getQName()); } broker.removeAllNodes(transaction, child, path, listener); if(child.getNodeType() == Node.ELEMENT_NODE) { path.removeLastComponent(); } } indexes.flush(); indexes.setMode(ReindexMode.STORE); indexes.getStreamListener(); broker.endRemove(transaction); children = i; final NodeId newNodeId = last == this ? nodeId.newChild() : last.getNodeId().nextSibling(); //Append new content appendChildren(transaction, newNodeId, null, new NodeImplRef(last), path, newContent, listener); broker.updateNode(transaction, this, false); indexes.reindex(transaction, reindexRoot, ReindexMode.STORE); broker.getValueIndex().reindex(valueReindexRoot); broker.flush(); } catch(final EXistException e) { LOG.warn("Exception while inserting node: " + e.getMessage(), e); } } /** * Update a child node. This method will only update the child node * but not its potential descendant nodes. * * @param oldChild * @param newChild * @throws DOMException */ @Override public IStoredNode updateChild(final Txn transaction, final Node oldChild, final Node newChild) throws DOMException { if((!(oldChild instanceof IStoredNode)) || (!(newChild instanceof IStoredNode))) { throw new DOMException(DOMException.WRONG_DOCUMENT_ERR, "Wrong node type"); } final IStoredNode<?> oldNode = (IStoredNode<?>) oldChild; final IStoredNode<?> newNode = (IStoredNode<?>) newChild; if(!oldNode.getNodeId().getParentId().equals(nodeId)) { throw new DOMException(DOMException.NOT_FOUND_ERR, "Node is not a child of this element"); } if(newNode.getNodeType() == Node.ATTRIBUTE_NODE && newNode.getQName().equals(Namespaces.XML_ID_QNAME)) { // an xml:id attribute. Normalize the attribute and set its type to ID final AttrImpl attr = (AttrImpl) newNode; attr.setValue(StringValue.trimWhitespace(StringValue.collapseWhitespace(attr.getValue()))); attr.setType(AttrImpl.ID); } IStoredNode<?> previousNode = (IStoredNode<?>) oldNode.getPreviousSibling(); if(previousNode == null) { previousNode = this; } else { previousNode = getLastNode(previousNode); } final NodePath currentPath = getPath(); final NodePath oldPath = oldNode.getPath(currentPath); try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker()) { final IndexController indexes = broker.getIndexController(); //May help getReindexRoot() to make some useful things indexes.setDocument(ownerDocument); //Check if the change affects any ancestor nodes, which then need to be reindexed later IStoredNode reindexRoot = indexes.getReindexRoot(oldNode, oldPath, false); indexes.setMode(ReindexMode.REMOVE_SOME_NODES); //Remove indexes if(reindexRoot == null) { reindexRoot = oldNode; } indexes.reindex(transaction, reindexRoot, ReindexMode.REMOVE_SOME_NODES); //TODO: fix once range index has been moved to new architecture final NativeValueIndex valueIndex = broker.getValueIndex(); final IStoredNode valueReindexRoot = valueIndex.getReindexRoot(this, oldPath); valueIndex.reindex(valueReindexRoot); //Remove the actual node data broker.removeNode(transaction, oldNode, oldPath, null); broker.endRemove(transaction); newNode.setNodeId(oldNode.getNodeId()); //Reinsert the new node data broker.insertNodeAfter(transaction, previousNode, newNode); final NodePath path = newNode.getPath(currentPath); broker.indexNode(transaction, newNode, path); if(newNode.getNodeType() == Node.ELEMENT_NODE) { broker.endElement(newNode, path, null); } broker.updateNode(transaction, this, true); //Recreate indexes on ancestor nodes indexes.reindex(transaction, reindexRoot, ReindexMode.STORE); valueIndex.reindex(valueReindexRoot); broker.flush(); } catch(final EXistException e) { LOG.warn("Exception while inserting node: " + e.getMessage(), e); } return newNode; } @Override public Node removeChild(final Txn transaction, final Node oldChild) throws DOMException { if(!(oldChild instanceof IStoredNode)) { throw new DOMException(DOMException.WRONG_DOCUMENT_ERR, "wrong node type"); } final IStoredNode<?> oldNode = (IStoredNode<?>) oldChild; if(!oldNode.getNodeId().getParentId().equals(nodeId)) { throw new DOMException(DOMException.NOT_FOUND_ERR, "node is not a child of this element"); } final NodePath oldPath = oldNode.getPath(); try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker()) { final IndexController indexes = broker.getIndexController(); indexes.setDocument(ownerDocument); final IStoredNode reindexRoot = indexes.getReindexRoot(oldNode, oldPath, false); indexes.setMode(ReindexMode.REMOVE_SOME_NODES); final StreamListener listener; if(reindexRoot == null) { listener = indexes.getStreamListener(); } else { listener = null; indexes.reindex(transaction, reindexRoot, ReindexMode.REMOVE_SOME_NODES); } broker.removeAllNodes(transaction, oldNode, oldPath, listener); --children; if(oldChild.getNodeType() == Node.ATTRIBUTE_NODE) { --attributes; } broker.endRemove(transaction); setDirty(true); broker.updateNode(transaction, this, false); broker.flush(); if(reindexRoot != null && !reindexRoot.getNodeId().equals(oldNode.getNodeId())) { indexes.reindex(transaction, reindexRoot, ReindexMode.STORE); } } catch(final EXistException e) { LOG.warn("Exception while inserting node: " + e.getMessage(), e); } return oldNode; } public void removeAppendAttributes(final Txn transaction, final NodeList removeList, final NodeList appendList) { try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker()) { final IndexController indexes = broker.getIndexController(); if(removeList != null) { try { for(int i = 0; i < removeList.getLength(); i++) { final Node oldChild = removeList.item(i); if(!(oldChild instanceof IStoredNode)) { throw new DOMException(DOMException.WRONG_DOCUMENT_ERR, "Wrong node type"); } final IStoredNode<?> old = (IStoredNode<?>) oldChild; if(!old.getNodeId().isChildOf(nodeId)) { throw new DOMException(DOMException.NOT_FOUND_ERR, "node " + old.getNodeId().getParentId() + " is not a child of element " + nodeId); } final NodePath oldPath = old.getPath(); // remove old custom indexes indexes.reindex(transaction, old, ReindexMode.REMOVE_SOME_NODES); broker.removeNode(transaction, old, oldPath, null); children--; attributes--; } } finally { broker.endRemove(transaction); } } final NodePath path = getPath(); indexes.setDocument(ownerDocument, ReindexMode.STORE); final IStoredNode reindexRoot = indexes.getReindexRoot(this, path, true, true); final StreamListener listener = reindexRoot == null ? indexes.getStreamListener() : null; if (children == 0) { appendChildren(transaction, nodeId.newChild(), null, new NodeImplRef(this), path, appendList, listener); } else { if(attributes == 0) { final IStoredNode<?> firstChild = (IStoredNode<?>) getFirstChild(); final NodeId newNodeId = firstChild.getNodeId().insertBefore(); appendChildren(transaction, newNodeId, firstChild.getNodeId(), new NodeImplRef(this), path, appendList, listener); } else { final AttribVisitor visitor = new AttribVisitor(); accept(visitor); final NodeId firstChildId = visitor.firstChild == null ? null : visitor.firstChild.getNodeId(); final NodeId newNodeId = visitor.lastAttrib.getNodeId().insertNode(firstChildId); appendChildren(transaction, newNodeId, firstChildId, new NodeImplRef(visitor.lastAttrib), path, appendList, listener); } setDirty(true); } attributes += appendList.getLength(); broker.updateNode(transaction, this, true); broker.flush(); indexes.reindex(transaction, reindexRoot, ReindexMode.STORE); } catch (final EXistException e) { LOG.warn("Exception while inserting node: " + e.getMessage(), e); } } private class AttribVisitor implements NodeVisitor { private IStoredNode lastAttrib = null; private IStoredNode firstChild = null; @Override public boolean visit(final IStoredNode node) { if(node.getNodeType() == Node.ATTRIBUTE_NODE) { lastAttrib = node; } else if(node.getNodeId().isChildOf(ElementImpl.this.nodeId)) { firstChild = node; return false; } return true; } } /** * Replaces the oldNode with the newChild * * @param transaction * @param newChild * @param oldChild * @return The new node (this differs from the {@link org.w3c.dom.Node#replaceChild(Node, Node)} specification) * @see org.w3c.dom.Node#replaceChild(org.w3c.dom.Node, org.w3c.dom.Node) */ @Override public Node replaceChild(final Txn transaction, final Node newChild, final Node oldChild) throws DOMException { if(!(oldChild instanceof IStoredNode)) { throw new DOMException(DOMException.WRONG_DOCUMENT_ERR, "Wrong node type"); } final IStoredNode<?> oldNode = (IStoredNode<?>) oldChild; if(!oldNode.getNodeId().getParentId().equals(nodeId)) { throw new DOMException(DOMException.NOT_FOUND_ERR, "Node is not a child of this element"); } IStoredNode<?> previous = (IStoredNode<?>) oldNode.getPreviousSibling(); if(previous == null) { previous = this; } else { previous = getLastNode(previous); } final NodePath oldPath = oldNode.getPath(); StreamListener listener = null; Node newNode = null; try(final DBBroker broker = ownerDocument.getBrokerPool().getBroker()) { final IndexController indexes = broker.getIndexController(); //May help getReindexRoot() to make some useful things indexes.setDocument(ownerDocument); final IStoredNode reindexRoot = broker.getIndexController().getReindexRoot(oldNode, oldPath, false); indexes.setMode(ReindexMode.REMOVE_SOME_NODES); if(reindexRoot == null) { listener = indexes.getStreamListener(); } else { indexes.reindex(transaction, reindexRoot, ReindexMode.REMOVE_SOME_NODES); } broker.removeAllNodes(transaction, oldNode, oldPath, listener); broker.endRemove(transaction); broker.flush(); indexes.setMode(ReindexMode.STORE); listener = indexes.getStreamListener(); newNode = appendChild(transaction, oldNode.getNodeId(), new NodeImplRef(previous), getPath(), newChild, listener); //Reindex if required broker.storeXMLResource(transaction, getOwnerDocument()); broker.updateNode(transaction, this, false); indexes.reindex(transaction, reindexRoot, ReindexMode.STORE); broker.flush(); } catch(final EXistException e) { LOG.warn("Exception while inserting node: " + e.getMessage(), e); } //return oldChild; // method is spec'd to return the old child, even though that's probably useless in this case return newNode; //returning the newNode is more sensible than returning the oldNode } private String escapeXml(final Node child) { final String str = ((Attr) child).getValue(); StringBuilder buffer = null; String entity; char ch; for(int i = 0; i < str.length(); i++) { ch = str.charAt(i); switch(ch) { case '"': entity = """; break; case '<': entity = "<"; break; case '>': entity = ">"; break; case '\'': entity = "'"; break; default: entity = null; break; } if(buffer == null) { if(entity != null) { buffer = new StringBuilder(str.length() + 20); buffer.append(str.substring(0, i)); buffer.append(entity); } } else { if(entity == null) { buffer.append(ch); } else { buffer.append(entity); } } } return buffer == null ? str : buffer.toString(); } public void setPreserveSpace(final boolean preserveWS) { this.preserveWS = preserveWS; } public boolean preserveSpace() { return preserveWS; } @Override public TypeInfo getSchemaTypeInfo() { throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "getSchemaTypeInfo() not implemented on class " + getClass().getName()); } @Override public void setIdAttribute(final String name, final boolean isId) throws DOMException { throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "setIdAttribute(String name, boolean isId) not implemented on class " + getClass().getName()); } @Override public void setIdAttributeNS(final String namespaceURI, final String localName, final boolean isId) throws DOMException { throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "setIdAttributeNS(String namespaceURI, String localName," + " boolean isId) not implemented on class " + getClass().getName()); } @Override public void setIdAttributeNode(final Attr idAttr, final boolean isId) throws DOMException { throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "setIdAttributeNode(Attr idAttr, boolean isId) not implemented on class " + getClass().getName()); } @Override public String getBaseURI() { final XmldbURI baseURI = calculateBaseURI(); if(baseURI != null) { return baseURI.toString(); } return ""; //UNDERSTAND: is it ok? } //TODO Please, keep in sync with org.exist.dom.memtree.ElementImpl private XmldbURI calculateBaseURI() { XmldbURI baseURI = null; final String nodeBaseURI = _getAttributeNS(Namespaces.XML_NS, "base"); if(nodeBaseURI != null) { baseURI = XmldbURI.create(nodeBaseURI, false); if(baseURI.isAbsolute()) { return baseURI; } } final IStoredNode parent = getParentStoredNode(); if(parent != null) { if(nodeBaseURI == null) { baseURI = ((ElementImpl) parent).calculateBaseURI(); } else { XmldbURI parentsBaseURI = ((ElementImpl) parent).calculateBaseURI(); if(nodeBaseURI.isEmpty()) { baseURI = parentsBaseURI; } else { baseURI = parentsBaseURI.append(baseURI); } } } else { if(nodeBaseURI == null) { return XmldbURI.create(getOwnerDocument().getBaseURI(), false); } else { final String docBaseURI = getOwnerDocument().getBaseURI(); if(docBaseURI.endsWith("/")) { baseURI = XmldbURI.create(getOwnerDocument().getBaseURI(), false); baseURI.append(baseURI); } else { baseURI = XmldbURI.create(getOwnerDocument().getBaseURI(), false); baseURI = baseURI.removeLastSegment(); baseURI.append(baseURI); } } } return baseURI; } @Override public boolean accept(final INodeIterator iterator, final NodeVisitor visitor) { if(!visitor.visit(this)) { return false; } if(hasChildNodes()) { final int childCount = getChildCount(); for(int i = 0; i < childCount; i++) { final IStoredNode next = iterator.next(); if(!next.accept(iterator, visitor)) { return false; } } } return true; } }