/* 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.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.gdata.model.ElementKey; import com.google.gdata.model.MetadataKey; import com.google.gdata.model.QName; import java.util.List; /** * The Path class represents an immutable path to a model entity in the GData * DOM. A path can be absolute (the root {@link ElementMetadata} for the path is * specified at construction time or relative (the root element type is unknown * at construction time). * <p> * The {@link #toAbsolute(ElementMetadata)} method can be used to produce an * absolute path from a relative path by interpreting it relative to a root. * <p> * New paths can be constructed using the {@link #of(MetadataKey...)} or * {@link #to(ElementMetadata, MetadataKey...)} methods as well as using the * {@link #builder()} method to obtain a new {@link Builder} instance that can * be used for incremental path construction. * * */ public class Path { /** * A simple relative path that selects the root of the path. */ public static final Path ROOT = builder().build(); /** * The Builder class provides a model for incrementally constructing new * {@link Path} relative or absolute instances. */ public static class Builder { private ElementMetadata<?, ?> root; private List<MetadataKey<?>> steps = Lists.newArrayList(); private boolean selectsAttribute; private ElementMetadata<?, ?> selectedElement; private AttributeMetadata<?> selectedAttribute; // Constructed using Path.builder() private Builder() {} /** * Specifies the root element type that any path steps should be interpreted * as being relative to. This will replace any existing root element type * for the builder and any existing steps will be revalidated relative t * the new root element type. * * @param root root element type for path * @return this builder instance (for chaining) * @throws PathException if this path has been bound to a * metadata instance and no key with the given step can be found. * @throws NullPointerException if root is null. */ Builder fromRoot(ElementMetadata<?, ?> root) { this.selectedElement = this.root = Preconditions.checkNotNull(root); if (steps != null) { // Recompute steps relative to the new root List<MetadataKey<?>> prevSteps = Lists.newArrayList(steps); steps.clear(); for (MetadataKey<?> step : prevSteps) { addStep(step); } } return this; } /** * Adds a new path step to the end of the path. If the path is absolute the * step will be validated against the element type currently selected by the * path. If the step parameter is an {@link ElementKey}, then the step will * be valid if there is a child element type with the same {@link QName}. If * the step parameter is an {@link AttributeKey}, then it will be valid if * there is a matching child attribute type with the same {@link QName}. If * invalid, a {@link PathException} will be thrown. No validation is * performed for relative paths. * * @param step metadata key for new path step * @throws PathException if this path has been bound to a * metadata instance and no key with the given step can be found, or * if the path is an attribute path. Once a path has an attribute key * no more steps may be added. */ public Builder addStep(MetadataKey<?> step) { if (selectedElement != null) { if (step instanceof ElementKey) { if (!addIfElement(step.getId())) { throw new PathException("No child element matching key:" + step); } } else { if (!addIfAttribute(step.getId())) { throw new PathException("No child attribute matching key:" + step); } } } else { // Unconditionally add steps to a relative path addToStepList(step); } return this; } /** * Conditionally adds a new element path step. For absolute paths, it will * be added if the {@link QName} matches a valid child element type for the * current selected element. If the path is relative a new * {@link ElementKey} step corresponding to the name will be unconditionally * added. * * @param id qualified name of child element step to add * @return {@code true} if added successfully, {@code false} otherwise. * @throws PathException if this path is an attribute path. Once a path has * an attribute key no more steps may be added. */ public boolean addIfElement(QName id) { ElementKey<?, ?> elemKey; if (selectedElement != null) { elemKey = selectedElement.findElement(id); if (elemKey == null) { return false; } selectedElement = selectedElement.bindElement(elemKey); } else { elemKey = ElementKey.of(id); } addToStepList(elemKey); return true; } /** * Conditionally adds a new attribute path step. For absolute paths, it will * be added if the {@link QName} matches a valid child attribute type for * the current selected element. If the path is relative a new * {@link ElementKey} step corresponding to the name will be unconditionally * added. * * @param id qualified name of child attribute step to add * @return {@code true} if added successfully, {@code false} otherwise. * @throws PathException if this path is an attribute path. Once a path has * an attribute key no more steps may be added. */ public boolean addIfAttribute(QName id) { AttributeKey<?> attrKey; if (selectedElement != null) { attrKey = selectedElement.findAttribute(id); if (attrKey == null) { return false; } selectedAttribute = selectedElement.bindAttribute(attrKey); } else { attrKey = AttributeKey.of(id); } addToStepList(attrKey); return true; } /** * Adds a single step to the end of the steps list. * * @throws PathException if this path is an attribute path. Once a path has * an attribute key no more steps may be added. */ private void addToStepList(MetadataKey<?> step) { if (selectsAttribute) { throw new PathException( "Cannot add to an attribute path: " +step.getId()); } if (step instanceof AttributeKey) { selectsAttribute = true; } steps.add(step); } /** * Returns a new {@link Path} instance based upon the current state of the * builder. */ public Path build() { // No exception thrown here because the path is validated as its built return new Path(this); } } /* * Returns a new {link Builder} instance for path construction. */ public static Builder builder() { return new Builder(); } /** * Constructs a new relative {@link Path} that selects an element defined by * the set of path steps to the element. * * @param steps keys defining steps to the selected element * @return selection path to element */ public static Path of(MetadataKey<?> ... steps) { Builder builder = new Builder(); for (MetadataKey<?> step : steps) { builder.addStep(step); } return builder.build(); } /** * Constructs a new absolute {@link Path} to an element type as defined by a * root type and the relative steps from it to the selected type. * * @param keys keys defining steps to the selected element * @return selection path to element * @throws PathException if this path has been bound to a * metadata instance and no key with the given step can be found, or * if the path is an attribute path. Once a path has an attribute key * no more steps may be added. * @throws NullPointerException if root is null. */ public static Path to(ElementMetadata<?, ?> root, MetadataKey<?> ... keys) { Builder builder = new Builder().fromRoot(root); for (MetadataKey<?> key : keys) { builder.addStep(key); } return builder.build(); } /** root element type of the path (if absolute) or null (if relative) */ private final ElementMetadata<?, ?> root; /** path steps */ private final List<MetadataKey<?>> steps; /** true if the path selects an attribute, false otherwise */ private final boolean selectsAttribute; /** element selected by the path or null (if relative) */ private final ElementMetadata<?, ?> selectedElement; /** attribute selected by the path or null (if relative or an element path */ private final AttributeMetadata<?> selectedAttribute; /** * Constructs a new {@link Path} based upon the state of the provided * {@link Builder} instance. */ private Path(Builder builder) { root = builder.root; steps = ImmutableList.copyOf(builder.steps); selectsAttribute = builder.selectsAttribute; if (root == null) { selectedElement = null; selectedAttribute = null; } else { selectedElement = builder.selectedElement; selectedAttribute = builder.selectedAttribute; } } /** * Returns {@code true} if the path selects an attribute. */ public boolean selectsAttribute() { return selectsAttribute; } /** * Returns {@code true} if the path selects an element. */ public boolean selectsElement() { return !selectsAttribute; } /** * Returns the element type currently selected by the path or {@code null} * if the path is relative. */ public ElementMetadata<?, ?> getSelectedElement() { return selectedElement; } /** * Returns the attribute type currently selected by the path or {@code null} * if the path selects an element or is relative. */ public AttributeMetadata<?> getSelectedAttribute() { return selectedAttribute; } /** * Returns the list of path steps. */ public List<MetadataKey<?>> getSteps() { return steps; } /** * Returns {@code true} if path is relative */ public boolean isRelative() { return root == null; } /** * Returns the attribute key at the end of the path. * * @throws IllegalStateException if this path is not to an attribute. */ public AttributeKey<?> getSelectedAttributeKey() { Preconditions.checkState(selectsAttribute, "Must select an attribute key."); return (AttributeKey<?>) steps.get(steps.size() - 1); } /** * Returns the element key at the end of the path. * * @throws IllegalStateException if this path is not to an element. */ public ElementKey<?, ?> getSelectedElementKey() { Preconditions.checkState(!steps.isEmpty(), "Must not be an empty path."); Preconditions.checkState(!selectsAttribute, "Must select an element key."); return (ElementKey<?, ?>) steps.get(steps.size() - 1); } /** * Returns the element key for the second-to-last key in the path. If the * path is only a single step this method will return {@code null}. */ public ElementKey<?, ?> getParentKey() { if (steps.size() > 1) { return (ElementKey<?, ?>) steps.get(steps.size() - 2); } return null; } /** * Constructs a new {@link Path} instance by interpreting the steps in the * current path relative to the provided root {@link ElementMetadata}. * * @param root root of returned path * @return new absolute {@link Path} bound to the root element metadata. * @throws PathException if the path is not found in the metadata. * @throws NullPointerException if root is null. */ public Path toAbsolute(ElementMetadata<?, ?> root) { Builder builder = new Builder().fromRoot(root); for (MetadataKey<?> step : steps) { builder.addStep(step); } return builder.build(); } /** * The {@link #toString()} implementation is overridden to return the XPath * string that represents the path. */ @Override public String toString() { if (steps.isEmpty()) { return "."; } StringBuilder builder = new StringBuilder(); for (MetadataKey<?> step : steps) { addPathSeparator(builder); if (step instanceof AttributeKey) { builder.append('@'); } builder.append(step.getId()); } return builder.toString(); } /** * Conditionally adds a path separator character to the builder if it has any * accumulated path. */ private void addPathSeparator(StringBuilder builder) { if (builder.length() != 0) { builder.append('/'); } } /** * The equals method will return true if the target object is also a * {@link Path}, has the same root or is also relative, and has the same list * of path steps. */ @Override public boolean equals(Object o) { if (o == null || o.getClass() != Path.class) { return false; } Path path = (Path) o; return root == path.root && steps.equals(path.steps); } @Override public int hashCode() { return Objects.hashCode(root, steps); } }