package org.exist.fluent; import java.util.HashMap; import java.util.Map; import org.apache.log4j.Logger; import org.exist.dom.NodeListImpl; import org.w3c.dom.*; /** * Allows attributes to be added to, replaced in and removed from an existing * element in the database. The updates are batched for efficiency; you must call * {@link #commit()} to apply them to the database. * * @author <a href="mailto:piotr@ideanest.com">Piotr Kaminski</a> */ public class AttributeBuilder { interface CompletedCallback { public void completed(NodeList removeList, NodeList addList); } private static final Logger LOG = Logger.getLogger(AttributeBuilder.class); private final CompletedCallback callback; private final NamespaceMap namespaceBindings; private final Element element; private boolean done; private final org.w3c.dom.Document doc; private final NodeListImpl removedAttributes = new NodeListImpl(), addedAttributes = new NodeListImpl(); private final Map<QName, Attr> removedMap = new HashMap<QName, Attr>(), addedMap = new HashMap<QName, Attr>(); AttributeBuilder(Element element, NamespaceMap namespaceBindings, CompletedCallback callback) { this.element = element; this.callback = callback; this.namespaceBindings = namespaceBindings.extend(); this.doc = ElementBuilder.createDocumentNode(); } private void checkDone() { if (done) throw new IllegalStateException("builder already done"); } /** * Add a namespace binding to this builder's namespaces map. * * @param key the prefix to bind * @param uri the URI to bind it to * @return this attribute builder, for chaining calls */ public AttributeBuilder namespace(String key, String uri) { namespaceBindings.put(key, uri); return this; } /** * Create a new attribute or change the value of an existing one. Later calls will overwrite * the values set by earlier ones. * * @param name the name of the attribute * @param value the value of the attribute, if not a <code>String</code> will be converted using {@link DataUtils#toXMLString(Object)} * @return this attribute builder, for chaining calls */ public AttributeBuilder attr(String name, Object value) { checkDone(); QName qname = QName.parse(name, namespaceBindings, null); // if previously added, this value will overwrite so remove old attribute from list Attr attr = addedMap.get(qname); if (attr != null) addedAttributes.remove(attr); // should be removed iff it is currently set on the element attr = qname.getAttributeNode(element); if (attr != null) { if (!removedMap.containsKey(qname)) { removedAttributes.add(attr); removedMap.put(qname, attr); } } else { assert !removedMap.containsKey(qname); } attr = qname.createAttribute(doc); attr.setValue(DataUtils.toXMLString(value)); addedAttributes.add(attr); addedMap.put(qname, attr); return this; } /** * Add an attribute or change an existing attribute's value only if the given condition holds. * Behaves just like {@link #attr(String, Object)} if <code>condition</code> is true, does * nothing otherwise. * * @param condition the condition that must be satisfied before the attribute's value is set * @param name the name of the attribute * @param value the value of the attribute * @return this attribute builder, for chaining calls */ public AttributeBuilder attrIf(boolean condition, String name, Object value) { checkDone(); if (condition) attr(name, value); return this; } /** * Delete an attribute. Does nothing if the attribute does not exist. It's allowed (though probably * pointless) to delete attributes that were created using {@link #attr(String, Object)}, even if not * yet committed. * * @param name the name of the attribute to delete * @return this attribute builder, for chaining */ public AttributeBuilder delAttr(String name) { checkDone(); QName qname = QName.parse(name, namespaceBindings, null); // override any previous addition Attr attr = addedMap.get(qname); if (attr != null) { addedAttributes.remove(attr); addedMap.remove(qname); } // if currently set on the element, and not already listed for removal, remove it attr = qname.getAttributeNode(element); if (attr != null && !removedMap.containsKey(qname)) { removedAttributes.add(attr); removedMap.put(qname, attr); } return this; } /** * Commit the attribute changes recorded with the other methods to the database. */ public void commit() { checkDone(); done = true; if (removedAttributes.isEmpty() && addedAttributes.isEmpty()) return; callback.completed(removedAttributes, addedAttributes); } @Override protected void finalize() { if (!done) LOG.warn("disposed without commit"); } }