/* 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.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap.Builder; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import com.google.common.collect.Maps; import com.google.gdata.util.common.xml.XmlNamespace; import com.google.gdata.model.ElementCreatorImpl.AttributeInfo; import com.google.gdata.model.ElementCreatorImpl.ElementInfo; import com.google.gdata.util.ParseException; import com.google.gdata.wireformats.ContentCreationException; import com.google.gdata.wireformats.ObjectConverter; import java.util.Collection; import java.util.Iterator; import java.util.Map; import java.util.Set; /** * Immutable implementation of the element metadata. This class delegates to * the schema for binding to other contexts and for retrieving children, and * uses an {@link AdaptationRegistry} for dealing with adaptations. * * <p>Each instance of this class is bound to a specific schema, parent key, * element key, and metadata context, see {@link MetadataImpl}. * * */ final class ElementMetadataImpl<D, E extends Element> extends MetadataImpl<D> implements ElementMetadata<D, E> { private static final ElementValidator DEFAULT_VALIDATOR = new MetadataValidator(); // Element metadata properties. private final ElementKey<D, E> elemKey; private final ElementKey<D, E> sourceKey; private final Cardinality cardinality; private final boolean isContentRequired; private final ElementValidator validator; private final Object properties; private final VirtualElementHolder virtualElementHolder; private final boolean isFlattened; /** Metadata for element's attributes and child elements. */ private final ImmutableMap<QName, AttributeKey<?>> attributes; private final ImmutableMap<QName, AttributeKey<?>> renamedAttributes; private final ImmutableMap<QName, ElementKey<?, ?>> elements; private final ImmutableMap<QName, ElementKey<?, ?>> renamedElements; /** Adaptation helper for dealing with adaptors on this element. */ private final AdaptationRegistry adaptations; /** * Constructs a new immutable element metadata instance from the given * declared metadata. */ ElementMetadataImpl(Schema schema, ElementTransform transform, ElementKey<?, ?> parent, ElementKey<D, E> key, MetadataContext context) { super(schema, transform, parent, key, context); this.elemKey = key; TransformKey transformSource = transform.getSource(); if (transformSource != null) { // Use the ID of the transform source for the source key. ElementKey<D, E> transformSourceKey = ElementKey.of( transformSource.getKey().getId(), key.getDatatype(), key.getElementType()); if (transformSourceKey.equals(elemKey)) { this.sourceKey = elemKey; } else { this.sourceKey = transformSourceKey; } } else { this.sourceKey = elemKey; } transform = ElementTransform.mergeSource(schema, key, transform, context); this.cardinality = firstNonNull( transform.getCardinality(), Cardinality.SINGLE); this.isContentRequired = firstNonNull(transform.getContentRequired(), true); this.validator = firstNonNull(transform.getValidator(), DEFAULT_VALIDATOR); this.properties = transform.getProperties(); this.virtualElementHolder = transform.getVirtualElementHolder(); this.isFlattened = transform.isFlattened(); this.attributes = getAttributes(transform.getAttributes().values()); this.renamedAttributes = getRenamedAttributes(); this.elements = getElements(transform.getElements().values()); this.renamedElements = getRenamedElements(); if (transform.getAdaptations().isEmpty()) { this.adaptations = null; } else { this.adaptations = AdaptationRegistryFactory.create(schema, transform); } } /** * Creates an immutable map of attributes from the given collection of * attribute info objects. The info objects are used in transforms to allow * bumping attributes to the end of the list, to change their order, but once * we are creating the element metadata the order is set and we just need the * keys. */ private ImmutableMap<QName, AttributeKey<?>> getAttributes( Collection<AttributeInfo> infos) { Builder<QName, AttributeKey<?>> builder = ImmutableMap.builder(); for (AttributeInfo info : infos) { builder.put(info.key.getId(), info.key); } return builder.build(); } /** * Creates an immutable map of renamed attributes from the collection of * attribute keys. This binds the attributes and checks if they have a * different name under the context of this metadata, and returns any * attributes with an alternate name in the map. */ private ImmutableMap<QName, AttributeKey<?>> getRenamedAttributes() { Builder<QName, AttributeKey<?>> builder = ImmutableMap.builder(); for (AttributeKey<?> key : attributes.values()) { AttributeMetadata<?> bound = bindAttribute(key); QName boundName = bound.getName(); if (!boundName.equals(key.getId())) { builder.put(boundName, key); } } return builder.build(); } /** * Creates an immutable map of child elements from the given collection of * element info objects. The info objects are used in transforms to allow * bumping elements to the end of the list, to change their order, but once we * are creating the element metadata the order is set and we just need the * keys. */ private ImmutableMap<QName, ElementKey<?, ?>> getElements( Collection<ElementInfo> infos) { Builder<QName, ElementKey<?, ?>> builder = ImmutableMap.builder(); for (ElementInfo info : infos) { builder.put(info.key.getId(), info.key); } return builder.build(); } /** * Creates an immutable map of renamed child elements from the collection of * element keys. This gets the transform for each child and checks if there * is a new, different name, and if so creates a map of those new names back * to the appropriate key, for use during parsing. */ private ImmutableMap<QName, ElementKey<?, ?>> getRenamedElements() { Map<QName, ElementKey<?, ?>> renamed = Maps.newLinkedHashMap(); for (ElementKey<?, ?> key : elements.values()) { ElementTransform transform = schema.getTransform(sourceKey, key, context); QName childName = transform.getName(); if (childName != null && !childName.equals(key.getId())) { // We only use the first renamed element if multiple elements are named // the same. if (!renamed.containsKey(childName)) { renamed.put(childName, key); } } } return ImmutableMap.copyOf(renamed); } /** * Adapts this element metadata to a different kind, using the provided key. * Will return {@code null} if no adaptation on the given kind exists. If an * adaptation does exist it will bind the adaptation under the same parent and * context as this element. */ public ElementKey<?, ?> adapt(String kind) { return (adaptations != null) ? adaptations.getAdaptation(kind) : null; } /** * Returns true if the attributes contain the given attribute key, based on * its ID. */ public boolean isDeclared(AttributeKey<?> key) { return attributes.containsKey(key.getId()); } /** * Finds the most appropriate attribute metadata for parsing the given ID. * First looks locally, if that fails it then looks locally for a attribute * with the "*" local name (used to allow foo:* attribute metadata), and then * if that fails it looks in the adaptations. This allows us to parse an * attribute that is declared in adaptations as declared metadata instead * of undeclared metadata. If it still hasn't found an attribute this method * will return null. */ public AttributeKey<?> findAttribute(QName id) { // First check any renamed attributes, as those take precedence. if (!renamedAttributes.isEmpty()) { AttributeKey<?> attKey = renamedAttributes.get(id); if (attKey != null) { return attKey; } } if (!attributes.isEmpty()) { AttributeKey<?> attKey = attributes.get(id); if (attKey != null) { return attKey; } // For wildcarded ids, iterate and return the first matching attribute if (id.matchesAnyNamespace()) { for (Map.Entry<QName, AttributeKey<?>> attrEntry : attributes.entrySet()) { if (id.matches(attrEntry.getKey())) { return attrEntry.getValue(); } } } else if (!id.matchesAnyLocalName()) { // See if there is a foo:* match for the given id. attKey = attributes.get(toWildcardLocalName(id)); if (attKey != null) { return AttributeKey.of(id, attKey.getDatatype()); } } } if (adaptations != null) { AttributeKey<?> attKey = adaptations.findAttribute(id); if (attKey != null) { return attKey; } } return null; } /** * Returns {@code true} if the given element key is declared, by ID. */ public boolean isDeclared(ElementKey<?, ?> element) { return elements.containsKey(element.getId()); } /** * Finds the most appropriate child metadata for parsing the given ID. * First looks locally, if that fails it then looks locally for an element * with the "*" local name (used to allow foo:* element metadata), and then * if that fails it looks in the adaptations. This allows us to parse an * element that is declared in adaptations as declared metadata instead * of undeclared metadata. If after looking in the adaptations we still have * not found a key for the QName, we return null. */ public ElementKey<?, ?> findElement(QName id) { // First check any renamed elements, as those take precedence. if (!renamedElements.isEmpty()) { ElementKey<?, ?> childKey = renamedElements.get(id); if (childKey != null) { return childKey; } } if (!elements.isEmpty()) { ElementKey<?, ?> childKey = elements.get(id); if (childKey != null) { return childKey; } // For wildcarded ids, iterate and return the first matching element if (id.matchesAnyNamespace()) { for (Map.Entry<QName, ElementKey<?, ?>> elemEntry : elements.entrySet()) { if (id.matches(elemEntry.getKey())) { return elemEntry.getValue(); } } } else if (!id.matchesAnyLocalName()) { // See if there is a foo:* match for the provided namespace. childKey = elements.get(toWildcardLocalName(id)); if (childKey != null) { return ElementKey.of( id, childKey.getDatatype(), childKey.getElementType()); } } } if (adaptations != null) { ElementKey<?, ?> childKey = adaptations.findElement(id); if (childKey != null) { return childKey; } } return null; } /** * Binds this metadata to a different context. This just asks the registry * to handle the binding for us. */ public ElementMetadata<D, E> bind( MetadataContext context) { return schema.bind(parent, elemKey, context); } @Override public ElementKey<D, E> getKey() { return elemKey; } public Cardinality getCardinality() { return cardinality; } public boolean isContentRequired() { return isContentRequired; } public boolean isReferenced() { return isVisible(); } public boolean isSelected(Element e) { return isVisible(); } public boolean isFlattened() { return isFlattened; } public Object getProperties() { return properties; } public ElementValidator getValidator() { return validator; } public void validate(ValidationContext vc, Element e) { if (validator != null) { validator.validate(vc, e, this); } } public Iterator<Attribute> getAttributeIterator(Element element) { return element.getAttributeIterator(this); } public ImmutableCollection<AttributeKey<?>> getAttributes() { return attributes.values(); } public <K> AttributeMetadata<K> bindAttribute(AttributeKey<K> key) { return schema.bind(sourceKey, key, context); } public Iterator<Element> getElementIterator(Element element) { return element.getElementIterator(this); } public ImmutableCollection<ElementKey<?, ?>> getElements() { return elements.values(); } @SuppressWarnings("unchecked") public <K, L extends Element> ElementMetadata<K, L> bindElement( ElementKey<K, L> key) { return schema.bind(sourceKey, key, context); } @Override public Object generateValue(Element element, ElementMetadata<?, ?> metadata) { Object result = super.generateValue(element, metadata); if (result == null) { result = element.getTextValue(elemKey); } return result; } @Override public void parseValue(Element element, ElementMetadata<?, ?> metadata, Object value) throws ParseException { if (!super.parse(element, metadata, value)) { element.setTextValue( ObjectConverter.getValue(value, elemKey.getDatatype())); } } public SingleVirtualElement getSingleVirtualElement() { if (cardinality != Cardinality.SINGLE) { return null; } if (virtualElementHolder != null) { return virtualElementHolder.getSingleVirtualElement(); } return null; } public MultipleVirtualElement getMultipleVirtualElement() { if (cardinality == Cardinality.SINGLE) { return null; } if (virtualElementHolder != null) { return virtualElementHolder.getMultipleVirtualElement(); } return null; } public E createElement() throws ContentCreationException { return Element.createElement(elemKey); } public XmlNamespace getDefaultNamespace() { // The default implementation uses the default namespace for the in-use name return getName().getNs(); } /** * Set of namespaces referenced by this element's metadata and all child * attributes and elements. This field is lazily initialized by the first * call to getReferencedNamespaces(). */ private Collection<XmlNamespace> referencedNamespaces = null; public Collection<XmlNamespace> getReferencedNamespaces() { // The referencedNamespaces field is lazily initialized because it is // only required for top-level types. A race condition is // harmless and will just result in multiple computations. if (referencedNamespaces == null) { ImmutableSet.Builder<XmlNamespace> builder = ImmutableSet.builder(); Set<ElementKey<?, ?>> added = Sets.newHashSet(); addReferencedNamespaces(this, builder, added); referencedNamespaces = builder.build(); } return referencedNamespaces; } private static void addReferencedNamespaces(ElementMetadata<?, ?> metadata, ImmutableSet.Builder<XmlNamespace> builder, Set<ElementKey<?, ?>> added) { // Avoid recursive looping if (added.contains(metadata.getKey())) { return; } added.add(metadata.getKey()); // Add namespace for this element (if any) XmlNamespace elemNs = metadata.getName().getNs(); if (elemNs != null) { builder.add(elemNs); } // Add namespace for all attributes (if any) for (AttributeKey<?> attrKey : metadata.getAttributes()) { AttributeMetadata<?> attrMetadata = metadata.bindAttribute(attrKey); XmlNamespace attrNs = attrMetadata.getName().getNs(); if (attrNs != null) { builder.add(attrNs); } } // Add namespace for all child elements (recursively) for (ElementKey<?, ?> elemKey : metadata.getElements()) { ElementMetadata<?, ?> childMetadata = metadata.bindElement(elemKey); addReferencedNamespaces(childMetadata, builder, added); } } /** * Returns an id of the form ns:*, if the given id does not already represent * the * localname. */ private QName toWildcardLocalName(QName id) { return new QName(id.getNs(), QName.ANY_LOCALNAME); } }