/* 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.Preconditions; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.common.collect.ImmutableSet.Builder; import com.google.gdata.model.ElementMetadata.Cardinality; import com.google.gdata.model.ElementMetadata.SingleVirtualElement; import com.google.gdata.model.ElementMetadata.MultipleVirtualElement; import com.google.gdata.model.Metadata.VirtualValue; import java.util.Collection; import java.util.Map; import java.util.Set; /** * Implementation of {@link ElementCreator}. This class is thread-safe for * writing, so multiple threads can modify the same element metadata without * introducing race conditions. * * */ final class ElementCreatorImpl extends MetadataCreatorImpl implements ElementCreator { /** The action to take with a new attribute or element. */ enum Action { ADD, REPLACE } /** An invalid name for the markers. */ private static final QName UNDECLARED = new QName("**UNDECLARED**"); /** Marker, see {@link #addUndeclaredAttributeMarker()} */ static final AttributeKey<Void> ATTRIBUTE_MARKER = AttributeKey.of(UNDECLARED, Void.class); /** Marker, see {@link #addUndeclaredElementMarker()} */ static final ElementKey<Void, Element> ELEMENT_MARKER = ElementKey.of(UNDECLARED, Void.class, Element.class); // The element key for this builder, so we know have the parent key for // child elements and attributes. private final ElementKey<?, ?> elementKey; // These fields are modifiable, and may only be changed via the set* methods. private Cardinality cardinality; private Boolean contentRequired; private ElementValidator validator; private Object properties; private VirtualElementHolder virtualElementHolder; private boolean flattened; private final Map<QName, AttributeInfo> attributes = Maps.newLinkedHashMap(); private final Map<QName, ElementInfo> elements = Maps.newLinkedHashMap(); private final Map<String, ElementKey<?, ?>> adaptations = Maps.newLinkedHashMap(); private Set<AttributeKey<?>> attributeWhitelist; private Set<ElementKey<?, ?>> elementWhitelist; /** * Constructs an empty element creator. */ ElementCreatorImpl(MetadataRegistry registry, TransformKey transformKey) { super(registry, transformKey); MetadataKey<?> key = transformKey.getKey(); Preconditions.checkArgument(key instanceof ElementKey<?, ?>, "Key must be to an element."); this.elementKey = (ElementKey<?, ?>) key; } /** * Merges the values from an existing element creator. */ void merge(ElementCreatorImpl other) { super.merge(other); if (other.cardinality != null) { this.cardinality = other.cardinality; } if (other.contentRequired != null) { this.contentRequired = other.contentRequired; } if (other.validator != null) { this.validator = other.validator; } if (other.properties != null) { this.properties = other.properties; } if (other.virtualElementHolder != null) { this.virtualElementHolder = other.virtualElementHolder; } if (other.flattened) { this.flattened = true; } // We copy the attributes and elements over by re-adding them, as they need // to get the actual metadata builders from the registry. for (AttributeInfo info : other.attributes.values()) { addAttribute(info.key, info.action); } for (ElementInfo info : other.elements.values()) { addElement(info.key, info.action); } // Adaptations map contains only immutable objects, copy away! adaptations.putAll(other.adaptations); // Merge the local whitelists with the other whitelists. if (other.attributeWhitelist != null) { whitelistAttributes(other.attributeWhitelist); } if (other.elementWhitelist != null) { whitelistElements(other.elementWhitelist); } } /** * Adds an adaptation to the given kind. */ public ElementCreatorImpl adapt(String kind, ElementKey<?, ?> adaptation) { synchronized (registry) { adaptations.put(kind, adaptation); registry.register(adaptation); registry.dirty(); } return this; } /** * Sets the cardinality of the element. The cardinality can be * {@link Cardinality#SINGLE} for a single element, * {@link Cardinality#MULTIPLE} for repeating elements, or * {@link Cardinality#SET} for a set of elements (duplicates not allowed). * * @param cardinality the cardinality of the element. * @return this element metadata builder for chaining. */ public ElementCreatorImpl setCardinality(Cardinality cardinality) { synchronized (registry) { this.cardinality = cardinality; registry.dirty(); } return this; } /** * Sets whether the element's content is required. By default the content is * required if the datatype is non-null. * * @param contentRequired true to set the content to required, false to set * it to optional. * @return this element metadata builder for chaining. */ public ElementCreatorImpl setContentRequired(boolean contentRequired) { synchronized (registry) { this.contentRequired = contentRequired; registry.dirty(); } return this; } /** * Sets the element's validator. The validator is used to check that the * element has all required attributes/elements/values. By default an * instance of {@link MetadataValidator} is used. * * @param validator element validator to use when validating the element, or * null if no validation is needed (for undeclared metadata). * @return this element metadata builder for chaining. */ public ElementCreatorImpl setValidator(ElementValidator validator) { synchronized (registry) { this.validator = validator; registry.dirty(); } return this; } /** * Sets the element's properties. This is used to provide additional * information during parsing/generation, and is specific to the wire format. * Because we are creating the default metadata here, this will be the * properties for the default wireformat. * * @param properties default properties for the element. * @return this element metadata builder for chaining. */ public ElementCreatorImpl setProperties(Object properties) { synchronized (registry) { this.properties = properties; registry.dirty(); } return this; } /** * Sets the virtual element for the element. This is used to create a * fully virtual element that doesn't map directly to the DOM. * * <p>This is used for single cardinality elements. Use * {@link MultipleVirtualElement} for multiple cardinality elements. */ public ElementCreatorImpl setSingleVirtualElement( SingleVirtualElement singleVirtualElement) { synchronized (registry) { this.virtualElementHolder = VirtualElementHolder.of(singleVirtualElement); registry.dirty(); } return this; } /** * Sets the virtual element for the element. This is used to create a * fully virtual element that doesn't map directly to the DOM. * * <p>This is used for single cardinality elements. Use * {@link MultipleVirtualElement} for multiple cardinality elements. */ public ElementCreatorImpl setMultipleVirtualElement( MultipleVirtualElement multipleVirtualElement) { synchronized (registry) { this.virtualElementHolder = VirtualElementHolder.of(multipleVirtualElement); registry.dirty(); } return this; } /** * Sets the source path of an element. An element that has a source will * use a virtual element based on the path. */ @Override void setSource(Path path, TransformKey key) { super.setSource(path, key); setElementPath(path); } /** * Sets the virtual element for this creator based on the path. */ private void setElementPath(Path path) { synchronized (registry) { this.virtualElementHolder = VirtualElementHolder.of(path); registry.dirty(); } } /** * Flattens this element, which just marks the builder as flattened. */ public ElementCreatorImpl flatten() { synchronized (registry) { this.flattened = true; registry.dirty(); } return this; } /** * Adds a virtual attribute, which marks the source attribute as moved and * creates a new virtual attribute in this element with the source path. */ public AttributeCreator moveAttribute(AttributeKey<?> attKey, Path path) { Preconditions.checkArgument(path.selectsAttribute(), "Path must refer to an attribute."); AttributeCreatorImpl dest = replaceAttribute(attKey); AttributeCreatorImpl source = getAttributeCreator(path); dest.setSource(path, source.getTransformKey()); source.moved(); return dest; } /** * Returns the attribute creator at the end of the path by looking it up * directly in the registry. */ private AttributeCreatorImpl getAttributeCreator(Path path) { Preconditions.checkArgument(path.selectsAttribute(), "Must be an attribute path"); ElementKey<?, ?> parent = path.getParentKey(); if (parent == null) { parent = elementKey; } AttributeKey<?> selected = path.getSelectedAttributeKey(); return (AttributeCreatorImpl) registry.build( parent, selected, transformKey.getContext()); } /** * Adds a virtual element, which marks the source element as moved and creates * a new virtual child element with the source path. */ public ElementCreator moveElement(ElementKey<?, ?> childKey, Path path) { Preconditions.checkArgument(path.selectsElement(), "Path must refer to an element."); ElementCreatorImpl dest = replaceElement(childKey); ElementCreatorImpl source = getElementCreator(path); dest.setSource(path, source.getTransformKey()); source.moved(); return dest; } /** * Returns the element creator at the end of the path by looking it up * directly in the registry (not traversing). */ private ElementCreatorImpl getElementCreator(Path path) { Preconditions.checkArgument(path.selectsElement(), "Must be an element path"); ElementKey<?, ?> parent = path.getParentKey(); if (parent == null) { parent = elementKey; } ElementKey<?, ?> selected = path.getSelectedElementKey(); return (ElementCreatorImpl) registry.build( parent, selected, transformKey.getContext()); } /** * Sets the location of the undeclared attributes. By default, undeclared * attributes appear after all declared attributes, this lets them appear * earlier in the list. */ public ElementCreatorImpl addUndeclaredAttributeMarker() { addAttribute(ATTRIBUTE_MARKER); return this; } /** * Add the key for an attribute. If an attribute with the same ID * already exists, the previous attribute will be removed, and the new * attribute will be added to the end of the list. If you want to replace the * existing element, use {@link #replaceAttribute(AttributeKey)}. * * @param attributeKey the key to the attribute that is being added. * @return an attribute builder that can be used to set the attribute fields. */ public AttributeCreatorImpl addAttribute(AttributeKey<?> attributeKey) { return addAttribute(attributeKey, Action.ADD); } /** * Replaces the existing metadata for an attribute. * * @param attributeKey the key to the attribute that is being replaced. * @return an attribute builder that can be used to modify the attribute. */ public AttributeCreatorImpl replaceAttribute(AttributeKey<?> attributeKey) { return addAttribute(attributeKey, Action.REPLACE); } /** * Whitelists a set of attributes as well as setting their order. Adding an * attribute pushes it to the end of the stack, so we just add the attributes * in the order given and then whitelist them. */ public ElementCreatorImpl orderAndWhitelistAttributes( AttributeKey<?>... attributeKeys) { for (AttributeKey<?> attributeKey : attributeKeys) { addAttribute(attributeKey); } return whitelistAttributes(attributeKeys); } /** * Whitelists a set of attributes for this element metadata. This will hide * all declared attributes on the metadata instance that will be created from * this builder. */ public ElementCreatorImpl whitelistAttributes( AttributeKey<?>... attributeKeys) { return whitelistAttributes(Lists.newArrayList(attributeKeys)); } /** * Whitelists a set of attributes for this element metadata. This will hide * all declared attributes on the metadata instance that will be created from * this builder. */ private ElementCreatorImpl whitelistAttributes( Collection<AttributeKey<?>> attributeKeys) { synchronized (registry) { if (attributeWhitelist == null) { attributeWhitelist = Sets.newHashSet(); } attributeWhitelist.addAll(attributeKeys); registry.dirty(); } return this; } /** * Adds an attribute to the map of attributes for this element. If this is * an add we will place the new attribute on the end of the list, and remove * any existing attributes with the same key. If this is not an add, it is * a replace, which will fail if the existing element does not exist. */ private AttributeCreatorImpl addAttribute( AttributeKey<?> attributeKey, Action action) { synchronized (registry) { QName id = attributeKey.getId(); if (action == Action.ADD) { attributes.remove(id); } attributes.put(id, new AttributeInfo(attributeKey, action)); return (AttributeCreatorImpl) registry.build( elementKey, attributeKey, transformKey.getContext()); } } /** * Sets the location of the undeclared elements. By default, undeclared * elements appear after all declared elements, this lets them appear * earlier in the list. */ public ElementCreatorImpl addUndeclaredElementMarker() { addElement(ELEMENT_MARKER); return this; } /** * Add the metadata for a child element. If an element with the same ID * already exists, the previous element will be removed, and the new element * will be added to the end of the list. If you want to replace the existing * element, use {@link #replaceElement(ElementKey)}. * * @param elementKey the key we are adding or pushing to the end. * @return the builder for the child element. */ public ElementCreatorImpl addElement(ElementKey<?, ?> elementKey) { return addElement(elementKey, Action.ADD); } /** * Replaces the existing metadata for a child element. * * @param elementKey the key we are replacing. * @return this element metadata builder for chaining. * @throws IllegalArgumentException if the child metadata doesn't exist. */ public ElementCreatorImpl replaceElement(ElementKey<?, ?> elementKey) { return addElement(elementKey, Action.REPLACE); } /** * Whitelists a set of child elements as well as setting their order. Adding * an element pushes it to the end of the stack, so we just add the child * elements in the order given and then whitelist them. */ public ElementCreatorImpl orderAndWhitelistElements( ElementKey<?,?>... elementKeys) { for (ElementKey<?, ?> elementKey : elementKeys) { addElement(elementKey); } return whitelistElements(elementKeys); } /** * Whitelists a set of child elements for this element metadata. This will * hide all declared child elements on the metadata instance that will be * created from this builder. */ public ElementCreatorImpl whitelistElements(ElementKey<?, ?>... elementKeys) { return whitelistElements(Lists.newArrayList(elementKeys)); } /** * Whitelists a set of child elements for this element metadata. This will * hide all declared child elements on the metadata instance that will be * created from this builder. */ private ElementCreatorImpl whitelistElements( Collection<ElementKey<?, ?>> elementKeys) { synchronized (registry) { if (elementWhitelist == null) { elementWhitelist = Sets.newHashSet(); } elementWhitelist.addAll(elementKeys); registry.dirty(); } return this; } /** * Blacklist a set of keys, these keys will be explicitly hidden from view. */ public ElementCreatorImpl blacklistElements(ElementKey<?, ?>... elementKeys) { synchronized (registry) { for (ElementKey<?, ?> elementKey : elementKeys) { addElement(elementKey).setVisible(false); } } return this; } /** * Adds an element to the map of elements we are modifying with this builder. */ private ElementCreatorImpl addElement( ElementKey<?, ?> childKey, Action action) { synchronized (registry) { QName id = childKey.getId(); Preconditions.checkNotNull(id); if (action == Action.ADD) { elements.remove(id); } elements.put(id, new ElementInfo(childKey, action)); return (ElementCreatorImpl) registry.build( elementKey, childKey, transformKey.getContext()); } } @Override public ElementCreatorImpl setName(QName name) { return (ElementCreatorImpl) super.setName(name); } @Override public ElementCreatorImpl setRequired(boolean required) { return (ElementCreatorImpl) super.setRequired(required); } @Override public ElementCreatorImpl setVisible(boolean visible) { return (ElementCreatorImpl) super.setVisible(visible); } @Override public ElementCreatorImpl setVirtualValue(VirtualValue virtualValue) { return (ElementCreatorImpl) super.setVirtualValue(virtualValue); } // Package-level read-only access to the fields of this creator. Cardinality getCardinality() { return cardinality; } Boolean getContentRequired() { return contentRequired; } ElementValidator getValidator() { return validator; } Object getProperties() { return properties; } VirtualElementHolder getVirtualElementHolder() { return virtualElementHolder; } boolean isFlattened() { return flattened; } /** * Returns an immutable copy of the attributes in this creator. */ Map<QName, AttributeInfo> getAttributes() { return ImmutableMap.copyOf(attributes); } /** * Returns an immutable set of attribute keys stored in this creator. */ ImmutableSet<AttributeKey<?>> getAttributeSet() { Collection<AttributeInfo> infos = attributes.values(); Builder<AttributeKey<?>> builder = ImmutableSet.builder(); for (AttributeInfo info : infos) { builder.add(info.key); } return builder.build(); } /** * Returns an immutable copy of the elements in this creator. */ Map<QName, ElementInfo> getElements() { return ImmutableMap.copyOf(elements); } /** * Returns an immutable set of element keys stored in this creator. */ ImmutableSet<ElementKey<?, ?>> getElementSet() { Collection<ElementInfo> infos = elements.values(); Builder<ElementKey<?, ?>> builder = ImmutableSet.builder(); for (ElementInfo info : infos) { builder.add(info.key); } return builder.build(); } /** * Returns an immutable copy of the adaptations in this creator. */ Map<String, ElementKey<?, ?>> getAdaptations() { return ImmutableMap.copyOf(adaptations); } /** * Returns an immutable copy of the whitelist of attributes in this creator. */ Set<AttributeKey<?>> getAttributeWhitelist() { return attributeWhitelist == null ? null : ImmutableSet.copyOf(attributeWhitelist); } /** * Returns an immutable copy of the whitelist of elements in this creator. */ Set<ElementKey<?, ?>> getElementWhitelist() { return elementWhitelist == null ? null : ImmutableSet.copyOf(elementWhitelist); } /** * Create a transform from this element metadata builder. */ ElementTransform toTransform() { check(); return ElementTransform.create(this); } /** * Makes sure the state is consistent, throws {@link IllegalStateException} if * it notices something wrong. */ private void check() { if (virtualElementHolder != null && cardinality != null) { if (cardinality == Cardinality.SINGLE) { if (virtualElementHolder.getSingleVirtualElement() == null) { throw new IllegalStateException( "Invalid element transform. " + "MultipleVirtualElement set on an element " + "with single cardinality for key " + elementKey); } } else { if (virtualElementHolder.getMultipleVirtualElement() == null) { throw new IllegalStateException( "Invalid element transform. " + "SingleVirtualElement set on an element " + "with multiple cardinality for key " + elementKey); } } } } /** * Holder for attribute information. Stores the key + any additional * information we want to keep track of. Currently this is just whether this * was an add or a replace. */ static final class AttributeInfo { final AttributeKey<?> key; final Action action; AttributeInfo(AttributeKey<?> key) { this(key, Action.REPLACE); } AttributeInfo(AttributeKey<?> key, Action action) { this.key = key; this.action = action; } } /** * Holder for element information. Stores the key + any additional * information we want to keep track of. Currently this is just whether this * was an add or a replace. */ static final class ElementInfo { final ElementKey<?, ?> key; final Action action; ElementInfo(ElementKey<?, ?> key) { this(key, Action.REPLACE); } ElementInfo(ElementKey<?, ?> key, Action action) { this.key = key; this.action = action; } } }