/* Copyright (c) 2008 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.gdata.model; import com.google.gdata.util.common.base.Objects; import com.google.gdata.util.common.base.Objects.ToStringHelper; import com.google.gdata.util.common.base.Pair; import com.google.gdata.util.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.gdata.util.common.xml.XmlNamespace; import com.google.gdata.model.ElementMetadata.Cardinality; import com.google.gdata.model.atom.Category; import com.google.gdata.util.ParseException; import com.google.gdata.wireformats.ContentCreationException; import com.google.gdata.wireformats.ContentValidationException; import com.google.gdata.wireformats.ObjectConverter; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.util.Collection; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; /** * Data element in an instance document. Contains attributes, * child elements, and a text node. * * <p>The various setter methods provided by this class return {@code this} * so setter invocations can be chained, as in the following example: * * <p><pre> * Element who = new Element(KEY) * .setAttributeValue(ATTR_KEY, "value") * .addElement( * new Element(EXT_KEY_NOEXT) * .setTextValue("yolk")); * </pre> * * <p>Subclasses are expected to follow the same model for any setter * methods they provide. * * @see #setTextValue(Object) * @see #setAttributeValue(AttributeKey, Object) * @see #addElement(Element) * @see #addElement(ElementKey, Element) */ public class Element { // Logger for logging warnings and errors. private static final Logger LOGGER = Logger.getLogger(Element.class.getName()); /** * Returns the default {@link ElementKey} for an {@link Element} type. * * @param type element type * @return default element key for type */ public static ElementKey<?, ?> getDefaultKey(Class<? extends Element> type) { Preconditions.checkNotNull(type, "type"); // The current approach used reflection based upon the implementation // pattern that every Element type will expose a static ElementKey field // named "KEY". ElementKey<?, ?> key = null; try { Field keyField = type.getField("KEY"); key = ElementKey.class.cast(keyField.get(null)); } catch (NoSuchFieldException nsfe) { throw new IllegalArgumentException("Unable to access KEY field:" + type, nsfe); } catch (IllegalArgumentException iae) { throw new IllegalArgumentException("Unable to access KEY field:" + type, iae); } catch (IllegalAccessException iae) { throw new IllegalArgumentException("Unable to access KEY field:" + type, iae); } catch (NullPointerException npe) { throw new IllegalArgumentException("Unable to access KEY field:" + type, npe); } return key; } /** * This class contains the element state, which is the attributes, elements, * and text content of this element. */ private static class ElementState { /** * Map of all attributes that were added to this element, in the order they * were added to the element. */ private Map<QName, Attribute> attributes; /** * Map of child elements, keyed by their ID. The value of a map * entry is either an instance of the class (for {@link Cardinality#SINGLE} * elements) or a list of instances (for {@link Cardinality#MULTIPLE}), or * a set of instances (for {@link Cardinality#SET}). This map is maintained * in the order the child elements were added to this element. */ private Map<QName, Object> elements; /** * Element's text node value. */ private Object value; /** * Indicates that the element has been locked. */ private volatile boolean locked; @Override public String toString() { ToStringHelper helper = Objects.toStringHelper(this); if (attributes != null) { helper.add("attributes", attributes.values()); } if (elements != null) { helper.add("elements", elements.values()); } if (value != null) { helper.add("value", value); } return helper.toString(); } } /** * The element key associated with this element. */ private final ElementKey<?, ?> key; /** * The state of this element, contains all actual data. This allows shallow * copies to be very efficient. */ private final ElementState state; /** * Construct element and associate with a key. * * @param elementKey the key to this element, contains the ID and datatype. */ public Element(ElementKey<?, ?> elementKey) { Preconditions.checkNotNull(elementKey, "elementKey"); this.key = bindKey(elementKey, getClass()); this.state = new ElementState(); } /** * Construct a generic undeclared element with the specified qualified name. * * @param qName qualified name */ public Element(QName qName) { this.key = ElementKey.of(qName, String.class, getClass()); this.state = new ElementState(); } /** * Copy constructor that initializes a new Element instance to be a wrapper * around another element instance. The element will use the given element * as its source for any content. * * @param elementKey the element key to associate with the copy. * @param source the element to copy data from. */ public Element(ElementKey<?, ?> elementKey, Element source) { this.key = bindKey(elementKey, getClass()); this.state = source.state; } /** * Binds an element key to a specific element subclass. This guarantees that * the key on an element will always have exactly that element's type as its * element type, and not some other element type. This makes it possible to * believe an element's key without needing to check the element type when * looking up metadata. */ private static ElementKey<?, ?> bindKey(ElementKey<?, ?> key, Class<? extends Element> type) { Class<?> keyType = key.getElementType(); if (keyType == type) { return key; } return ElementKey.of(key.getId(), key.getDatatype(), type); } /** * Returns true if this element has been locked using {@link #lock}. Once an * element has been locked it cannot be unlocked. */ public final boolean isLocked() { return state.locked; } /** * Locks this element. A locked element cannot have any changes made to its * content or its attributes or child elements. This will also lock all * attributes and child elements as well. Once this method has been called, * this element can be safely published to other threads. */ public Element lock() { state.locked = true; if (state.attributes != null) { for (Attribute att : state.attributes.values()) { att.lock(); } } if (state.elements != null) { for (Object childObj : state.elements.values()) { if (childObj instanceof Element) { ((Element) childObj).lock(); } else { for (Element child : castElementCollection(childObj)) { child.lock(); } } } } return this; } /** * Throws an {@link IllegalStateException} if this instance is locked. */ private void throwExceptionIfLocked() { Preconditions.checkState(!state.locked, "%s instance is read only", getElementId()); } /** * Returns the key to this element. */ public ElementKey<?, ?> getElementKey() { return key; } /** * Get the id of this element. */ public QName getElementId() { return key.getId(); } /** * Returns an iterator over all attributes on this element. */ public Iterator<Attribute> getAttributeIterator() { return getAttributeIterator(null); } /** * Returns an iterator over the attributes of this element with a well-defined * iteration order based on the metadata. All declared attributes are * returned first, in the order of declaration, followed by undeclared * attributes in the order in which they were added to this element. If the * metadata declares virtual attributes, those attributes will be included in * the iterator, likewise any attributes which are hidden will be excluded. * * @param metadata the element metadata to use for iteration * @return an iterator over the attributes of this element */ public Iterator<Attribute> getAttributeIterator( ElementMetadata<?, ?> metadata) { return new AttributeIterator(this, metadata, state.attributes); } /** * Returns the number of attributes present on this element. * * @return count of attributes */ public int getAttributeCount() { return (state.attributes != null) ? state.attributes.size() : 0; } /** * Returns true if the element has an attribute with the given id. */ public boolean hasAttribute(QName id) { return (state.attributes == null) ? false : state.attributes.containsKey(id); } /** * Returns true if the element has an attribute with the given key. */ public boolean hasAttribute(AttributeKey<?> childKey) { return hasAttribute(childKey.getId()); } /** * Get the value of an attribute by id. */ public Object getAttributeValue(QName id) { if (state.attributes == null) { return null; } Attribute attribute = state.attributes.get(id); return (attribute == null) ? null : attribute.getValue(); } /** * Returns the attribute value cast to the appropriate type, based on the * given key. * * @param <T> return type * @param key the attribute key to use to cast the attribute value * @return typed attribute value * @throws IllegalArgumentException if the value cannot be converted to the * key type */ public <T> T getAttributeValue(AttributeKey<T> key) { Attribute attribute = (state.attributes == null) ? null : state.attributes.get(key.getId()); Object value = (attribute == null) ? null : attribute.getValue(); if (value == null) { return null; } try { return ObjectConverter.getValue(value, key.getDatatype()); } catch (ParseException e) { throw new IllegalArgumentException("Unable to convert value " + e + " to datatype " + key.getDatatype()); } } /** * Add attribute by id and value. If the value is {@code null} this is * equivalent to removing the attribute with the given id. */ public Element setAttributeValue(QName id, Object attrValue) { return setAttributeValue(AttributeKey.of(id), attrValue); } /** * Add attribute by value. If the value is {@code null} the value will be * removed. * * @param key attribute key that is being added * @param attrValue attribute value or {@code null} to remove */ public Element setAttributeValue(AttributeKey<?> key, Object attrValue) { if (attrValue == null) { removeAttributeValue(key); } else { setAttribute(key, new Attribute(key, attrValue)); } return this; } /** * Puts an attribute into the attribute map, creating the map if needed. */ private void setAttribute(AttributeKey<?> attKey, Attribute attribute) { throwExceptionIfLocked(); if (state.attributes == null) { state.attributes = new LinkedHashMap<QName, Attribute>(); } state.attributes.put(attKey.getId(), attribute); } /** * * @deprecated use removeAttributeValue instead. */ @Deprecated public Object removeAttribute(QName id) { return removeAttributeValue(id); } /** * Remove attribute (if present). * * @param id the qualified name of the attribute. * @return this element */ public Object removeAttributeValue(QName id) { throwExceptionIfLocked(); Attribute removed = (state.attributes == null) ? null : state.attributes.remove(id); return (removed == null) ? null : removed.getValue(); } /** * * @deprecated use removeAttributeValue instead. */ @Deprecated public Object removeAttribute(AttributeKey<?> key) { return removeAttributeValue(key); } /** * Remove attribute (if present). * * @param key the key of the attribute. * @return this element */ public Object removeAttributeValue(AttributeKey<?> key) { return removeAttributeValue(key.getId()); } /** * Returns an iterator over all child elements of this element. */ public Iterator<Element> getElementIterator() { return getElementIterator(null); } /** * Returns an iterator over all child elements with a well-defined iteration * order based on this metadata. All declared elements are returned first, in * the order of declaration, followed by undeclared elements in the order in * which they were added to this element. If the metadata declares virtual * elements, those elements will be included in the iterator, likewise any * elements which are hidden will be excluded. * * @param metadata the metadata to use for iteration * @return iterator over the child elements of the element */ public Iterator<Element> getElementIterator(ElementMetadata<?, ?> metadata) { return new ElementIterator(this, metadata, state.elements); } /** * Returns the number of child elements present on this element. * @return number of elements. */ public int getElementCount() { int elementCount = 0; if (state.elements != null) { for (Object elementValue : state.elements.values()) { if (elementValue instanceof Collection) { elementCount += (castElementCollection(elementValue)).size(); } else { elementCount++; } } } return elementCount; } /** * Get a child element matching the specified qualified name. * * @param id the qualified name of the child to retrieve * @return the matching child element, or {@code null} if none was found * @throws IllegalArgumentException if the id referenced a repeating element */ public Element getElement(QName id) { Object mapValue = getElementObject(id); if (mapValue instanceof Element) { return (Element) mapValue; } Preconditions.checkArgument(!(mapValue instanceof Collection<?>), "The getElement(*) method was called for a repeating element. " + "Use getElements(*) instead."); return null; } /** * Get child element matching the specified key. Will try to adapt the * element to the given key if it is not already an instance of the requested * class. This will fail with an exception if the adaptation was not valid. * * @param <T> the type of element to return * @param childKey the metadata key for the child element to retrieve * @return child element, or {@code null} if none was found * @throws IllegalArgumentException if the key referenced a repeating element */ public <D, T extends Element> T getElement(ElementKey<D, T> childKey) { Element child = getElement(childKey.getId()); if (child == null) { return null; } try { return adapt(childKey, child); } catch (ContentCreationException e) { throw new IllegalArgumentException("Unable to adapt to " + childKey.getElementType(), e); } } /** * This method just returns the bare object stored in the map, or null if * either the map didn't contain the object or the map is null. */ private Object getElementObject(QName id) { if (state.elements == null) { return null; } if ("*".equals(id.getLocalName())) { XmlNamespace ns = id.getNs(); if (ns != null) { String uri = ns.getUri(); ImmutableList.Builder<Element> builder = ImmutableList.builder(); for (Map.Entry<QName, Object> entry : state.elements.entrySet()) { QName key = entry.getKey(); XmlNamespace keyNs = key.getNs(); if (keyNs != null && uri.equals(keyNs.getUri())) { Object value = entry.getValue(); if (value instanceof Element) { builder.add((Element) value); } else { builder.addAll(castElementCollection(value)); } } } return builder.build(); } } return state.elements.get(id); } /** * This method just returns the bare object stored in the map, or null if * either the map didn't contain the object or the map is null. */ private Object getElementObject(ElementKey<?, ?> childKey) { return getElementObject(childKey.getId()); } /** * Convenience method to return child element's text node as an object. * Returns {@code null} if child doesn't exist or child does not have a text * node. */ public Object getElementValue(QName id) { Element e = getElement(id); return (e == null) ? null : e.getTextValue(); } /** * Convenience method to return child element's text node cast to * the specified type. Returns {@code null} if child element does * not exist or has no text node. * * @param <V> child element's text node type * @param key identifying the child element. * @return child element's text node, cast to type {@code V}, * or {@code null} if child element does not exist or has no * text node */ public <V> V getElementValue(ElementKey<V, ? extends Element> key) { Element e = getElement(key); return e == null ? null : e.getTextValue(key); } /** * Returns true if the element has child element(s) with the given id. */ public boolean hasElement(QName id) { return (state.elements == null) ? false : state.elements.containsKey(id); } /** * Returns true if the element has child element(s) with the given key. */ public boolean hasElement(ElementKey<?, ?> childKey) { return hasElement(childKey.getId()); } /** * Returns an immutable list of elements matching the given id. */ public List<Element> getElements(QName id) { // children, and if so add all of the wrapper junk we deal with in Multimap. ImmutableList.Builder<Element> builder = ImmutableList.builder(); Object obj = getElementObject(id); if (obj != null) { if (obj instanceof Element) { builder.add((Element) obj); } else { for (Element e : castElementCollection(obj)) { builder.add(e); } } } return builder.build(); } /** * Get child elements matching the specified key. This list cannot be * used to add new child elements, instead the {@link #addElement(Element)} * method should be used. If the elements at the given key are not of the * correct type an {@link IllegalArgumentException} will be thrown. * * @param key child key to lookup child elements based on. * @return element's children, or an empty list if there are no children with * the given key's id. */ public <T extends Element> List<T> getElements(ElementKey<?, T> key) { // children, and if so add all of the wrapper junk we deal with in Multimap. ImmutableList.Builder<T> builder = ImmutableList.builder(); Object obj = getElementObject(key); if (obj != null) { Class<? extends T> childType = key.getElementType(); if (obj instanceof Element) { if (childType.isInstance(obj)) { builder.add(childType.cast(obj)); } } else { // Returns a list of all children that matched the given key. // If we change to returning mutable lists this will need to be a // view of the underlying data instead. for (Element e : castElementCollection(obj)) { if (childType.isInstance(e)) { builder.add(childType.cast(e)); } } } } return builder.build(); } /** * Get child elements matching the specified id. This set cannot be used * to add new child elements, instead the {@link #addElement(Element)} method * should be used. */ public Set<Element> getElementSet(QName id) { // children, and if so add all of the wrapper junk we deal with in Multimap. ImmutableSet.Builder<Element> builder = ImmutableSet.builder(); Object obj = getElementObject(id); if (obj != null) { if (obj instanceof Element) { builder.add((Element) obj); } else { for (Element e : castElementCollection(obj)) { builder.add(e); } } } return builder.build(); } /** * Get child elements matching the specified key. This set cannot be * used to add new child elements, instead the {@link #addElement(Element)} * method should be used. If the elements at the given key are not of the * correct type an {@link IllegalArgumentException} will be thrown. * * @param key the child key to lookup child elements based on. * @return elements children, or an empty set if there are no children with * the given key's id. */ public <T extends Element> Set<T> getElementSet(ElementKey<?, T> key) { // children, and if so add all of the wrapper junk we deal with in Multimap. ImmutableSet.Builder<T> builder = ImmutableSet.builder(); Object obj = getElementObject(key); if (obj != null) { Class<? extends T> childType = key.getElementType(); if (obj instanceof Element) { if (childType.isInstance(obj)) { builder.add(childType.cast(obj)); } } else { // Returns a set of all children that matched the given key. // If we change to returning mutable lists this will need to be a // view of the underlying data instead. for (Element e : castElementCollection(obj)) { if (childType.isInstance(e)) { builder.add(childType.cast(e)); } } } } return builder.build(); } /** * Sets the value of the child element(s) with the given id. The given element * will replace all existing elements at the given id. If the given element * is {@code null}, this is equivalent to {@link #removeElement(QName)}. */ public Element setElement(QName id, Element element) { removeElement(id); if (element != null) { addElement(id, element); } return this; } /** * Sets a child element to the given value. Uses the element key of the * element as the key. This is equivalent to calling * {@code setElement(element.getElementKey(), element);}. * * @throws NullPointerException if element is null. */ public Element setElement(Element element) { Preconditions.checkNotNull(element); setElement(element.getElementKey(), element); return this; } /** * Sets the value of the child element(s) with the {@code key}. The * {@code element} will replace all existing elements with the same key. If * element is null, this is equivalent to {@link #removeElement(ElementKey)}. * * @param key the key for the child element * @param element child element * @return this element for chaining */ public Element setElement(ElementKey<?, ?> key, Element element) { removeElement(key); if (element != null) { addElement(key, element); } return this; } /** * Add a child element, using the key of the child element as the key into * this element's children. * * @param element child element * @return this element for chaining * @throws NullPointerException if element is null. */ public Element addElement(Element element) { Preconditions.checkNotNull(element); addElement(element.getElementKey(), element); return this; } /** * Add a child element with the given ID. This will add the given element to * the end of the collection of elements with the same ID. If you want to * replace any existing elements use {@link #setElement(QName, Element)} * instead. * * @param id the qualified name to use for the child * @param element child element * @return this element for chaining * @throws NullPointerException if element is null. */ public Element addElement(QName id, Element element) { Preconditions.checkNotNull(element); addElement(ElementKey.of(id, element.getElementKey().getDatatype(), element.getClass()), element); return this; } /** * Add a child element with the given key. This will add the given element to * the end of the collection of elements with the same ID. If you want to * replace any existing elements use {@link #setElement(ElementKey, Element)} * instead. * * @param key the key of the child. * @param element child element * @return this element for chaining */ public Element addElement(ElementKey<?, ?> key, Element element) { throwExceptionIfLocked(); if (state.elements == null) { state.elements = new LinkedHashMap<QName, Object>(); } ElementKey<?, ?> elementKey = element.getElementKey(); key = calculateKey(key, elementKey); if (!key.equals(elementKey)) { try { element = createElement(key, element); } catch (ContentCreationException e) { throw new IllegalArgumentException("Key " + key + " cannot be applied" + " to element with key " + elementKey); } } QName id = key.getId(); Object obj = state.elements.get(id); if (obj == null) { state.elements.put(id, element); } else if (obj instanceof Collection<?>) { Collection<Element> collect = castElementCollection(obj); collect.add(element); } else { Collection<Element> collect = createCollection(key); collect.add((Element) obj); collect.add(element); state.elements.put(id, collect); } return this; } /** * Calculates the actual key that should be used for adding an element. This * uses the ID and datatype of the key, but if the element types are in the * same type hierarchy the narrowest element type is used. */ private ElementKey<?, ?> calculateKey(ElementKey<?, ?> key, ElementKey<?, ?> sourceKey) { Class<?> keyType = key.getElementType(); Class<? extends Element> sourceType = sourceKey.getElementType(); // If the sourceType is a subtype of the key type, we want to use it // as the type of element that we create, because it is more specific but // still compatible. if (keyType != sourceType && keyType.isAssignableFrom(sourceType)) { key = ElementKey.of(key.getId(), key.getDatatype(), sourceType); } return key; } /** * Remove child element(s) of a given name. All elements with the given id * will be removed. * * @param id the id of the child element(s) to remove. * @return this element for chaining. */ public Element removeElement(QName id) { throwExceptionIfLocked(); if (state.elements != null) { state.elements.remove(id); } return this; } /** * Remove child element(s) of a given name. All elements with the same ID as * the given key will be removed. * * @param childKey key of the element(s) to remove. * @return this element for chaining. */ public Element removeElement(ElementKey<?, ?> childKey) { return removeElement(childKey.getId()); } /** * Remove a single child element from this element. This method returns true * if the element was found and removed, or false if it was not. It uses * identity and not equality to find a match. * * @param element the child element to remove. * @return true if the child element was removed from this element. */ public boolean removeElement(Element element) { return removeElement(element.getElementKey(), element); } /** * Remove a single child element from this element. This method returns true * if the element was found and removed, or false if it was not. It uses * identity and not equality to find a match. * * @param childKey the key for the child element to remove. * @param element the child element to remove. * @return true if the child element was removed from this element. */ public boolean removeElement(ElementKey<?, ?> childKey, Element element) { throwExceptionIfLocked(); boolean removed = false; if (state.elements != null) { Object obj = getElementObject(childKey); if (obj instanceof Collection<?>) { Collection<Element> collect = castElementCollection(obj); Iterator<Element> iter = collect.iterator(); while (iter.hasNext()) { if (iter.next() == element) { iter.remove(); removed = true; break; } } if (collect.isEmpty()) { removeElement(childKey); } } else if (obj == element) { removeElement(childKey); removed = true; } } return removed; } /** * Replace one element with another. If the element to add has the * same id as the one that is being replaced, it will be switch in place, * maintaining order in repeating or undeclared elements. * * @param toRemove element to remove. * @param toAdd element to add. * @return true if the replacement succeeded. */ public boolean replaceElement(Element toRemove, Element toAdd) { throwExceptionIfLocked(); // If toAdd is null, this is just a remove. if (toAdd == null) { return removeElement(toRemove); } // If the IDs do not match, we do a remove and then an add. QName id = toRemove.getElementId(); if (!id.equals(toAdd.getElementId())) { boolean removed = removeElement(toRemove); if (removed) { addElement(toAdd); } return removed; } // Matched IDs, try to find the removed element and replace it. if (state.elements != null) { Object obj = state.elements.get(id); if (obj instanceof List<?>) { List<Element> list = castElementList(obj); for (int i = 0; i < list.size(); i++) { if (list.get(i) == toRemove) { list.set(i, toAdd); return true; } } } else if (obj instanceof Set<?>) { Set<Element> set = castElementSet(obj); if (set.remove(toRemove)) { set.add(toAdd); } } else if (obj == toRemove) { state.elements.put(id, toAdd); return true; } } return false; } /** * Suppress the warnings around casting the object in the map into a list * of elements. */ @SuppressWarnings("unchecked") private <T extends Element> List<T> castElementList(Object obj) { return (List<T>) obj; } /** * Suppress the warnings around casting the object in the map into a set of * elements. */ @SuppressWarnings("unchecked") private <T extends Element> Set<T> castElementSet(Object obj) { return (Set<T>) obj; } /** * Suppress the warnings around casting the object in the map into a * collection of elements. */ @SuppressWarnings("unchecked") private <T extends Element> Collection<T> castElementCollection(Object obj) { return (Collection<T>) obj; } /** * Creates a collection based on the given key. */ private <T extends Element> Collection<T> createCollection( ElementKey<?, ?> key) { // is part of resolve? Class<?> elementType = key.getElementType(); if (Category.class.isAssignableFrom(elementType)) { return Sets.newLinkedHashSet(); } else { return Lists.newArrayList(); } } /** * Clears internal state of all attributes, child elements, and text content. */ public void clear() { throwExceptionIfLocked(); state.value = null; state.attributes = null; state.elements = null; } /** * Returns the untyped element value or null if it has no value. * * @return untyped element value */ public Object getTextValue() { return state.value; } /** * Returns the element value adapted to the key's datatype. * * @param <V> data type of the key. * @param key the element key used to convert the value. * @return typed element value. */ public <V> V getTextValue(ElementKey<V, ?> key) { if (state.value != null) { try { return ObjectConverter.getValue(state.value, key.getDatatype()); } catch (ParseException e) { throw new IllegalArgumentException("Unable to convert value " + e + " to datatype " + key.getDatatype()); } } return null; } /** * Sets the value of the element and returns the element to allow chaining. * * @param newValue element's value * @return this element * @throws IllegalStateException if the element is immutable * @throws IllegalArgumentException if the object is of an invalid type or * if this element does not allow a value */ public Element setTextValue(Object newValue) { throwExceptionIfLocked(); state.value = checkValue(key, newValue); return this; } /** * Checks that the datatype of this element allows setting the value to the * given object. Throws an {@link IllegalArgumentException} if the value * is not valid for this element. */ Object checkValue(ElementKey<?, ?> elementKey, Object newValue) { if (newValue != null) { Class<?> datatype = elementKey.getDatatype(); Preconditions.checkArgument(datatype != Void.class, "Element must not contain a text node"); Preconditions.checkArgument(datatype.isInstance(newValue), "Invalid class: %s", newValue.getClass().getCanonicalName()); } return newValue; } /** * @return true if element has a text node value */ public boolean hasTextValue() { return state.value != null; } /** * Resolve the state of all elements in the tree, rooted at this * element, against the metadata. Throws an exception if the tree * cannot be resolved. * * @param metadata the metadata to resolve against. * @return the narrowed element if narrowing took place * @throws ContentValidationException if tree cannot be resolved */ public Element resolve(ElementMetadata<?, ?> metadata) throws ContentValidationException { ValidationContext vc = new ValidationContext(); Element narrowed = resolve(metadata, vc); if (!vc.isValid()) { throw new ContentValidationException("Invalid data", vc); } return narrowed; } /** * Resolve this element's state against the metadata. Accumulates * errors in caller's validation context. * * @param vc validation context * @return the narrowed element if narrowing took place. */ public Element resolve(ElementMetadata<?, ?> metadata, ValidationContext vc) { // Return immediately if the metadata is null, no resolve necessary. if (metadata == null) { return this; } Element narrowed = narrow(metadata, vc); narrowed.validate(metadata, vc); // Resolve all child elements. Iterator<Element> childIterator = narrowed.getElementIterator(); if (childIterator.hasNext()) { List<Pair<Element, Element>> replacements = Lists.newArrayList(); while (childIterator.hasNext()) { Element child = childIterator.next(); ElementMetadata<?, ?> childMeta = metadata.bindElement( child.getElementKey()); Element resolved = child.resolve(childMeta, vc); if (resolved != child) { replacements.add(Pair.of(child, resolved)); } } // Replace any resolved child elements with their replacement. for (Pair<Element, Element> pair : replacements) { narrowed.replaceElement(pair.getFirst(), pair.getSecond()); } } return narrowed; } /** * Narrow down element's type to the most specific one possible. * <p> * Any validation errors discovered during narrowing are accumulated * in the validation context. * <p> * Default action is to not do anything with current element. * Subclasses may override this function to narrow the type * in some custom fashion. * * @param metadata the element metadata to narrow to. * @param vc validation context * @return element narrowed down to the most specific type */ protected Element narrow(ElementMetadata<?, ?> metadata, ValidationContext vc) { ElementKey<?, ?> narrowedKey = metadata.getKey(); Class<?> narrowedType = narrowedKey.getElementType(); if (!narrowedType.isInstance(this)) { // Make sure the narrowing is valid. if (!getClass().isAssignableFrom(narrowedType)) { LOGGER.severe("Element of type " + getClass() + " cannot be narrowed to type " + narrowedType); } // Adapt to the more narrow type. try { return adapt(narrowedKey, this); } catch (ContentCreationException e) { LOGGER.log(Level.SEVERE, "Unable to adapt " + getClass() + " to " + narrowedType, e); } } return this; } /** * Adapts an element based on a key. This will find an adaptation in the * metadata and adapt to that metadata type (and element type). If no * adaptation is found this will return the source element. * * @param source the element we are narrowing from. * @param sourceMeta the source metadata to adapt from. * @param kind the kind name to lookup the adaptation for. * @return the adapted element if one was found. */ protected Element adapt(Element source, ElementMetadata<?, ?> sourceMeta, String kind) { ElementKey<?, ?> adaptorKey = sourceMeta.adapt(kind); if (adaptorKey != null) { try { return adapt(adaptorKey, source); } catch (ContentCreationException e) { // Not usable as a adaptable kind, skip. LOGGER.log(Level.SEVERE, "Unable to adapt " + source.getClass() + " to " + adaptorKey.getElementType(), e); } } return source; } /** * Adapts an element based on a different key. If the key represents * a more narrow type than {@code source}, an instance of the more narrow * type will be returned, containing the same information as the source. If * the source is {@code null}, a null instance of {@code T} will be returned. * * @param <T> the type of element to adapt to. * @param key the element key to adapt to. * @param source the element we are adapting from. * @return the adapted element if one was found. * @throws ContentCreationException if the metadata cannot be used to adapt. * @throws NullPointerException if meta is null. */ protected <T extends Element> T adapt(ElementKey<?, T> key, Element source) throws ContentCreationException { Preconditions.checkNotNull(key); Class<? extends T> adaptingTo = key.getElementType(); if (source == null || adaptingTo.isInstance(source)) { return adaptingTo.cast(source); } Class<? extends Element> adaptingFrom = source.getClass(); Preconditions.checkArgument(adaptingFrom.isAssignableFrom(adaptingTo), "Cannot adapt from element of type %s to an element of type %s", adaptingFrom, adaptingTo); return createElement(key, source); } /** * Validate the element using the given metadata, and placing any errors into * the validation context. The default behavior is to use the metadata * validation, subclasses may override this to add their own validation. If * the metadata is null (undeclared), no validation will be performed. */ protected void validate(ElementMetadata<?, ?> metadata, ValidationContext vc) { if (metadata != null) { metadata.validate(vc, this); } } /** * Visits the element using the specified {@link ElementVisitor} and metadata. * A {@code null} metadata indicates that the element is undeclared, and * child elements will be visited in the order they were added to the element. * * @param ev the element visitor instance to use. * @param meta the metadata for the element, or {@code null} for undeclared * metadata. * @throws ElementVisitor.StoppedException if traversal must be stopped */ public void visit(ElementVisitor ev, ElementMetadata<?, ?> meta) { visit(ev, null, meta); } /** * Visit implementation, recursively visits this element and all of its * children. */ private void visit(ElementVisitor ev, Element parent, ElementMetadata<?, ?> meta) throws ElementVisitor.StoppedException { // Visit the current element. boolean visitChildren = ev.visit(parent, this, meta); if (visitChildren) { visitChildren(ev, meta); } ev.visitComplete(parent, this, meta); } /** * Visit all of the children of this element, calling the element visitor * with the child element and child metadata for each child. */ private void visitChildren(ElementVisitor ev, ElementMetadata<?, ?> meta) throws ElementVisitor.StoppedException { // Visit children Iterator<Element> childIterator = getElementIterator(meta); while (childIterator.hasNext()) { Element child = childIterator.next(); ElementMetadata<?, ?> childMeta = (meta == null) ? null : meta.bindElement(child.getElementKey()); child.visit(ev, this, childMeta); } } /** * @param o given object * @return true if the given object is not null and is the same concrete class * as this one */ protected boolean sameClassAs(Object o) { return o != null && getClass().equals(o.getClass()); } /** * Helper method to check for equality between two object, including null * checks. * * @param o1 object 1 or <code>null</code> * @param o2 object 2 or <code>null</code> * @return true if the specified arguments are equal, or both null */ protected static boolean eq(Object o1, Object o2) { return o1 == null ? o2 == null : o1.equals(o2); } /** * Helper method that constructs a new {@link Element} instance of the type * defined by the type parameter {@code E}. * * @param key the element key to create the element from * @return element that was created * @throws ContentCreationException if content cannot be created */ public static <E extends Element> E createElement( ElementKey<?, E> key) throws ContentCreationException { return createElement(key, null); } /** * Helper method that constructs a new {@link Element} instance of the type * defined by the type parameter {@code E}. * * @param key the element key to create the element for. * @param source the source element to use, or null if a fresh instance should * be created. * @return element that was created * @throws ContentCreationException if content cannot be created */ public static <E extends Element> E createElement( ElementKey<?, E> key, Element source) throws ContentCreationException { if (source != null && key.equals(source.getElementKey()) && key.getElementType().isInstance(source)) { return key.getElementType().cast(source); } Class<?>[] argTypes; Object[] args; Class<? extends E> elementClass = key.getElementType(); try { try { // First try the constructor that takes in {@link ElementKey}. if (source != null) { argTypes = new Class<?>[] {ElementKey.class, source.getClass()}; args = new Object[] {key, source}; } else { argTypes = new Class<?>[] {ElementKey.class}; args = new Object[] {key}; } return construct(elementClass, argTypes, args); } catch (NoSuchMethodException e) { // Now try the null-arg or 1-arg constructor. if (source != null) { argTypes = new Class<?>[] {source.getClass()}; args = new Object[] {source}; } else { argTypes = new Class<?>[] {}; args = new Object[] {}; } return construct(elementClass, argTypes, args); } } catch (NoSuchMethodException e) { throw new ContentCreationException( "Constructor not found: " + elementClass); } catch (IllegalAccessException e) { throw new ContentCreationException( "Constructor not found: " + elementClass); } catch (InstantiationException e) { throw new ContentCreationException( "Constructor not found: " + elementClass); } catch (InvocationTargetException e) { throw new ContentCreationException( "Constructor not found: " + elementClass, e.getCause()); } } /** * Attempt to construct an instance of the given class with the given args * and arg types. Will set the constructor to accessible, allowing access * to non-public constructors, so use with caution. */ private static <T> T construct(Class<? extends T> clazz, Class<?>[] argTypes, Object[] args) throws SecurityException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { @SuppressWarnings("unchecked") Constructor<T>[] ctcs = (Constructor<T>[]) clazz.getDeclaredConstructors(); for (Constructor<T> ctc : ctcs) { Class<?>[] paramTypes = ctc.getParameterTypes(); if (paramsValid(paramTypes, argTypes)) { ctc.setAccessible(true); return ctc.newInstance(args); } } // We didn't find a constructor, this will report an error consistent // with not finding a valid public constructor. return clazz.getConstructor(argTypes).newInstance(args); } private static boolean paramsValid(Class<?>[] paramTypes, Class<?>[] argTypes) { if (paramTypes.length != argTypes.length) { return false; } for (int i = 0; i < paramTypes.length; i++) { if (!paramTypes[i].isAssignableFrom(argTypes[i])) { return false; } } return true; } @Override public int hashCode() { return state.hashCode(); } @Override public boolean equals(Object obj) { if (!(obj instanceof Element)) { return false; } return state.equals(((Element) obj).state); } @Override public String toString() { ToStringHelper helper = Objects.toStringHelper(this); helper.addValue(getElementId() + "@" + Integer.toHexString(hashCode())); Iterator<Attribute> aIter = getAttributeIterator(); while (aIter.hasNext()) { Attribute att = aIter.next(); helper.add(att.getAttributeKey().getId().toString(), att.getValue()); } if (hasTextValue()) { helper.addValue(getTextValue()); } return helper.toString(); } }