/* 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.ImmutableList;
import com.google.common.collect.Lists;
import com.google.gdata.model.ElementMetadata.Cardinality;
import com.google.gdata.model.ElementMetadata.MultipleVirtualElement;
import com.google.gdata.model.ElementMetadata.SingleVirtualElement;
import com.google.gdata.model.Metadata.VirtualValue;
import com.google.gdata.util.ParseException;
import com.google.gdata.wireformats.ContentCreationException;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import javax.annotation.Nullable;
/**
* A static factory for adapting from {@link Path} instances to instances of
* {@link VirtualValue}, {@link SingleVirtualElement} or
* {@link MultipleVirtualElement}. We adapt to both single and multiple at the
* same time because we don't know if the path is singular or multiple until it
* is bound to metadata, and these paths are used during the creation of
* metadata.
*
* they would be useful for other path contexts. Perhaps "Paths"?
*
*
*/
class PathAdapter {
/**
* Creates an adapter that implements both {@link SingleVirtualElement} and
* {@link MultipleVirtualElement} based on the path.
*/
static ElementAdapter elementAdapter(Path path) {
return new ElementAdapter(path);
}
/**
* Creates an adapter that implements {@link VirtualValue} based on the path.
*/
static ValueAdapter valueAdapter(Path path) {
return new ValueAdapter(path);
}
/**
* Returns element at the second-to-last position along the path, starting
* with the given root element. If at any point there is no valid element
* this method will return {@code null}.
*
* @throws NullPointerException if the root or path is null.
* @throws IllegalArgumentException if there are any repeating elements along
* the path.
*/
static Element getParentElement(Path path, Element rootElement) {
Preconditions.checkNotNull(path, "path");
Preconditions.checkNotNull(rootElement, "rootElement");
List<MetadataKey<?>> steps = path.getSteps();
if (steps.isEmpty()) {
return null;
}
Element parent = rootElement;
for (int i = 0; i < steps.size() - 1; i++) {
ElementKey<?, ?> childKey = (ElementKey<?, ?>) steps.get(i);
parent = parent.getElement(childKey);
if (parent == null) {
return null;
}
}
return parent;
}
/**
* Returns the last element along the path, starting with the given root
* element. If at any point there is no valid element this method will return
* {@code null}. If the path ends in an attribute key this will return the
* parent of that attribute, if it ends in an element key it will return that
* element.
*
* @throws NullPointerException if the root or path is null.
* @throws IllegalArgumentException if there are any repeating elements along
* the path.
*/
static Element getFinalElement(Path path, Element rootElement) {
Element parent = getParentElement(path, rootElement);
if (path.selectsAttribute() || parent == null) {
return parent;
}
return parent.getElement(path.getSelectedElementKey());
}
/**
* Returns a collection of parent elements along the path. This will throw
* an exception if the path had multiple steps where the child was multiple
* cardinality.
*/
static Collection<? extends Element> getFinalElements(Path path,
Element rootElement) {
List<? extends Element> parents = ImmutableList.of(rootElement);
for (MetadataKey<?> part : path.getSteps()) {
if (part instanceof AttributeKey<?>) {
break;
}
ElementKey<?, ?> childKey = (ElementKey<?, ?>) part;
// If we already have multiple parents, all remaining elements must be
// singular.
if (parents.size() > 1) {
List<Element> next = Lists.newArrayListWithCapacity(parents.size());
for (Element e : parents) {
next.add(e.getElement(childKey));
}
parents = next;
} else {
// If we only have a single parent, we get all children.
Element parent = parents.get(0);
parents = parents.get(0).getElements(childKey);
}
if (parents.isEmpty()) {
return parents;
}
}
return parents;
}
/**
* Travels along the path, creating any elements that don't exist until it
* gets to the second-to-last key along the path.
*/
static Element createParentElement(Path path, Element rootElement)
throws ParseException {
Preconditions.checkNotNull(path, "path");
Preconditions.checkNotNull(rootElement, "rootElement");
List<MetadataKey<?>> steps = path.getSteps();
if (steps.isEmpty()) {
return null;
}
Element parent = rootElement;
for (int i = 0; i < steps.size() - 1; i++) {
ElementKey<?, ?> childKey = (ElementKey<?, ?>) steps.get(i);
parent = getOrCreateChild(parent, childKey);
}
return parent;
}
/**
* Creates an array of parent elements for the parent element. This method
* requires the metadata in hand because we need to know where any multiple
* cardinality elements show up in the path.
*/
static Collection<Element> createParentElements(Path path,
Element rootElement, ElementMetadata<?, ?> rootMetadata,
int elementCount) throws ParseException {
Preconditions.checkNotNull(path, "path");
Preconditions.checkNotNull(rootElement, "rootElement");
List<MetadataKey<?>> steps = path.getSteps();
if (steps.isEmpty()) {
return ImmutableList.of();
}
Element parent = rootElement;
List<Element> parents = null;
ElementMetadata<?, ?> parentMetadata = rootMetadata;
for (int i = 0; i < steps.size() - 1; i++) {
ElementKey<?, ?> childKey = (ElementKey<?, ?>) steps.get(i);
ElementMetadata<?, ?> childMetadata = (parentMetadata == null)
? null : parentMetadata.bindElement(childKey);
// If parents is non-null, we already found a multiple-cardinality
// element, so we create a single child element for each parent.
if (parents != null) {
checkNotMultiple(childMetadata);
List<Element> children = Lists.newArrayListWithCapacity(parents.size());
for (Element p : parents) {
children.add(getOrCreateChild(p, childKey));
}
parents = children;
parentMetadata = childMetadata;
continue;
}
// If we haven't found our multiple cardinality element, and this element
// is singular, we create a single child.
if (childMetadata == null
|| childMetadata.getCardinality() == Cardinality.SINGLE) {
Element child = getOrCreateChild(parent, childKey);
parent = child;
parentMetadata = childMetadata;
continue;
}
// This is our first multiple cardinality element, so we create the
// appropriate number of child elements and add them to the parent.
List<Element> children = Lists.newArrayListWithCapacity(elementCount);
for (int j = 0; j < elementCount; j++) {
try {
Element child = Element.createElement(childKey);
parent.addElement(child);
children.add(child);
} catch (ContentCreationException e) {
throw new ParseException(e);
}
}
parents = children;
parentMetadata = childMetadata;
}
if (parents != null) {
return parents;
}
return ImmutableList.of(parent);
}
/**
* Travels along the path, creating any elements that don't exist until it
* gets to the final key. If the final key is an element, it creates that
* element if necessary and returns it. If the final key is an attribute, it
* returns the parent of that attribute.
*/
static Element createFinalElement(Path path, Element rootElement)
throws ParseException {
Element parent = createParentElement(path, rootElement);
if (path.selectsAttribute() || parent == null) {
return parent;
}
ElementKey<?, ?> childKey = path.getSelectedElementKey();
Element child = parent.getElement(childKey);
if (child == null) {
try {
child = Element.createElement(childKey);
parent.addElement(child);
} catch (ContentCreationException e) {
throw new ParseException(e);
}
}
return child;
}
/**
* Gets an existing child from the parent or creates it if needed.
*/
private static Element getOrCreateChild(
Element parent, ElementKey<?, ?> childKey) throws ParseException {
Element child = parent.getElement(childKey);
if (child != null) {
return child;
}
try {
child = Element.createElement(childKey);
parent.addElement(child);
return child;
} catch (ContentCreationException e) {
throw new ParseException(e);
}
}
/**
* Checks that a particular piece of metadata is not multiple cardinality.
*
* @throws IllegalStateException if the metadata is multiple cardinality
*/
private static void checkNotMultiple(ElementMetadata<?, ?> meta) {
if (meta != null && meta.getCardinality() != Cardinality.SINGLE) {
throw new IllegalStateException("Metadata for key " + meta.getKey()
+ " represents a multiple-cardinality element."
+ " The path cannot contain more than one multiple-cardinality"
+ " element.");
}
}
/**
* Generates the value for a given attribute key and metadata. If the
* attribute metadata is null this will just return the attribute value, but
* if the metadata is not null it will have the metadata generate the value.
*/
static Object generateAttributeValue(
Element element, ElementMetadata<?, ?> metadata,
AttributeKey<?> attKey, AttributeMetadata<?> attMeta) {
if (metadata != null && attMeta != null) {
return attMeta.generateValue(element, metadata);
}
return element.getAttributeValue(attKey);
}
/**
* Parses the value for an attribute.
*/
static void parseAttributeValue(
Element element, ElementMetadata<?, ?> metadata,
AttributeKey<?> attKey, AttributeMetadata<?> attMeta, Object value)
throws ParseException {
if (attMeta == null) {
element.setAttributeValue(attKey, value);
} else {
attMeta.parseValue(element, metadata, value);
}
}
/**
* Generates the text value for a given key and metadata. If the metadata is
* null this will just return the text value, but if the metadata is not null
* it will have the metadata generate the value.
*/
static Object generateTextValue(
Element element, ElementMetadata<?, ?> metadata) {
if (metadata != null) {
return metadata.generateValue(element, metadata);
}
return element.getTextValue();
}
/**
* Parses the text content for an element.
*/
static void parseTextValue(
Element element, ElementMetadata<?, ?> metadata, Object value)
throws ParseException {
if (metadata != null) {
metadata.parseValue(element, metadata, value);
} else {
element.setTextValue(value);
}
}
/**
* An adapter to {@link SingleVirtualElement} and
* {@link MultipleVirtualElement}. Allows a path to be used as a virtual
* element for parsing and generation.
*/
static class ElementAdapter implements SingleVirtualElement,
MultipleVirtualElement {
// The underlying path.
private final Path path;
/**
* Constructs an element adapter with a particular path.
*/
ElementAdapter(Path path) {
this.path = path;
}
/**
* Generates a single element on the parent by using this path. This
* will follow the path to its end, and then use that same element content
* as the returned element, after wrapping it with the correct key (id).
*
* elements along it.
*/
public Element generateSingle(Element parent,
ElementMetadata<?, ?> parentMetadata, ElementMetadata<?, ?> metadata) {
Preconditions.checkState(path.selectsElement(),
"An attribute path cannot be used to generate elements.");
Element element = getFinalElement(path, parent);
if (element == null) {
return null;
}
try {
ElementKey<?, ?> realKey = mergeKeys(
element.getElementKey(), metadata.getKey());
return Element.createElement(realKey, element);
} catch (ContentCreationException e) {
throw new IllegalArgumentException("Invalid metadata", e);
}
}
/**
* Parses a single element using this path. This will follow the path until
* the second to last element on the path, and then parse into the final
* element using the real parent instead of the path's parent.
*
* elements along it.
*/
public void parse(Element parent, ElementMetadata<?, ?> parentMetadata,
Element element, ElementMetadata<?, ?> metadata) throws ParseException {
Preconditions.checkState(path.selectsElement(),
"An attribute path cannot be used to parse elements.");
parent = createParentElement(path, parent);
ElementKey<?, ?> lastKey = path.getSelectedElementKey();
try {
Element child = Element.createElement(lastKey, element);
parent.addElement(child);
} catch (ContentCreationException e) {
throw new ParseException(e);
}
}
/**
* Generate multiple elements based on this path. This only allows a single
* multiple cardinality element in the path, but that multiple cardinality
* element can be anywhere in the path. Once found we only follow single
* cardinality children after that point.
*
* cardinality along the path.
*/
public Collection<? extends Element> generateMultiple(Element parent,
ElementMetadata<?, ?> parentMetadata, ElementMetadata<?, ?> metadata) {
Preconditions.checkState(path.selectsElement(),
"An attribute path cannot be used to generate elements.");
Collection<? extends Element> elements = getFinalElements(path, parent);
if (elements.isEmpty()) {
return elements;
}
List<Element> result = Lists.newArrayListWithCapacity(elements.size());
for (Element e : elements) {
try {
ElementKey<?, ?> realKey = mergeKeys(
e.getElementKey(), metadata.getKey());
result.add(Element.createElement(realKey, e));
} catch (ContentCreationException ex) {
throw new IllegalArgumentException("Invalid metadata", ex);
}
}
return result;
}
/**
* Parse multiple elements using the path. Because the path only allows a
* single multiple cardinality element, we can correctly parse into the
* appropriate child locations by matching up our elements with the parents,
* and creating one element per passed in element once we hit a multiple
* cardinality path element.
*/
public void parse(Element parent, ElementMetadata<?, ?> parentMetadata,
Collection<Element> elements, ElementMetadata<?, ?> metadata)
throws ParseException {
Preconditions.checkState(path.selectsElement(),
"An attribute path cannot be used to parse elements.");
Path bound = path.toAbsolute(parentMetadata);
Collection<Element> parents = createParentElements(
bound, parent, parentMetadata, elements.size());
ElementKey<?, ?> childKey = bound.getSelectedElementKey();
ElementMetadata<?, ?> childMetadata = bound.getSelectedElement();
Iterator<Element> pIter = parents.iterator();
Iterator<Element> eIter = elements.iterator();
// If we have multiple parents, the child metadata must not be multiple.
if (parents.size() > 1) {
checkNotMultiple(childMetadata);
while (pIter.hasNext() && eIter.hasNext()) {
Element p = pIter.next();
p.addElement(eIter.next());
}
return;
}
// If we only have a single parent and more than one child, the child
// metadata must be multiple cardinality.
if (elements.size() > 1 && childMetadata != null
&& childMetadata.getCardinality() == Cardinality.SINGLE) {
throw new IllegalStateException("Metadata for key " + childKey
+ " represents a single-cardinality element."
+ " The path must contain at least one multiple-cardinality"
+ " element in order to parse multiple elements.");
}
// Add all elements to the single parent.
parent = pIter.next();
while (eIter.hasNext()) {
parent.addElement(eIter.next());
}
}
/**
* Merges the key from the metadata with the source key to get the "real"
* key for a moved element.
*/
private static ElementKey<?, ?> mergeKeys(ElementKey<?, ?> sourceKey,
ElementKey<?, ?> metadataKey) {
if (!metadataKey.getId().equals(sourceKey.getId())) {
return ElementKey.of(metadataKey.getId(), sourceKey.getDatatype(),
sourceKey.getElementType());
}
return sourceKey;
}
}
/**
* An adapter from a path to {VirtualValue}, this allows a path to represent
* the value of an attribute or element.
*/
static class ValueAdapter implements VirtualValue {
// The path being adapted.
private final Path path;
/**
* Constructs an adapter with a particular path.
*/
ValueAdapter(Path path) {
this.path = path;
}
/**
* Generate a text value through the path. If the path ends in an element,
* the value will be the text content of the final element. If the path
* ends in an attribute, the value will be the value of that attribute.
*/
public Object generate(Element element, ElementMetadata<?, ?> metadata) {
Path bound = path.toAbsolute(metadata);
element = getFinalElement(bound, element);
if (element == null) {
return null;
}
// Generate the value at the end of the path.
if (bound.selectsAttribute()) {
return generateAttributeValue(element, bound.getSelectedElement(),
bound.getSelectedAttributeKey(), bound.getSelectedAttribute());
} else {
return generateTextValue(element, bound.getSelectedElement());
}
}
/**
* Parses a value through a path. This will find the element or attribute
* that is at the end of the path, and parse the value into the element's
* text content or the attribute's value as appropriate.
*/
public void parse(Element element, ElementMetadata<?, ?> metadata,
Object value) throws ParseException {
Path bound = path.toAbsolute(metadata);
element = createFinalElement(path, element);
if (bound.selectsAttribute()) {
parseAttributeValue(element, bound.getSelectedElement(),
bound.getSelectedAttributeKey(), bound.getSelectedAttribute(),
value);
} else {
parseTextValue(element, bound.getSelectedElement(), value);
}
}
}
private PathAdapter() {}
}