/** * Copyright (C) 2014 CUSTIS (http://www.custis.ru/) * * 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 ru.custis.beanpath; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; import java.io.Serializable; import java.util.Collection; import java.util.Iterator; import java.util.LinkedList; import static com.google.common.base.Preconditions.checkNotNull; /** * Models chain of bean properties in object oriented manner */ @Immutable public class BeanPath<T> implements Iterable<BeanPath<?>>, Serializable { private static final long serialVersionUID = 42L; private final BeanPath<?> parent; private final String name; private final Class<T> type; private BeanPath(BeanPath<?> parent, String name, Class<T> type) { this.parent = parent; this.name = checkNotNull(name, "Argument 'name' must not be null"); this.type = checkNotNull(type, "Argument 'type' must not be null"); } /** * Creates root path of given {@code type}, with pseudo name {@code <root>} and no parent */ public static @Nonnull <T> BeanPath<T> root(@Nonnull Class<T> type) { checkNotNull(type, "Argument 'type' must not be null"); return new BeanPath<T>(null, "<root>", type); } /** * Appends an element to this path and returns the new path. * Creates new instance, leaves {@code this} intact. */ public @Nonnull <T1> BeanPath<T1> append(@Nonnull String name, @Nonnull Class<T1> type) { checkNotNull(name, "Argument 'name' must not be null"); checkNotNull(type, "Argument 'type' must not be null"); return new BeanPath<T1>(this, name, type); } /** * Is this path is root * <p/> * It honors invariants: * <pre><code> * this.isRoot() == (this.getParent() == null) * this.isRoot() == !this.hasParent() * </code></pre> */ public boolean isRoot() { return (parent == null); } /** * The root of the path, i.e. vacuous path with one element that represents a bean * on which property chain applicated. * <p/> * It honors invariants: * <pre><code> * (this.getRoot() == this) && this.isRoot() * (this.getRoot() == this.getRoot().getRoot()) // and so on * </code></pre> */ public @Nonnull BeanPath<?> getRoot() { return (parent == null) ? this : parent.getRoot(); } /** * Whether the path has a parent * <p/> * It honors invariants: * <pre><code> * this.hasParent() == (this.getParent() != null) * this.hasParent() == !this.isRoot() * </code></pre> */ public boolean hasParent() { return (parent != null); } /** * Parent path of this path, i.e. path without last (tail) property; * or {@code null} if {@code this.isRoot()} */ public @Nullable BeanPath<?> getParent() { return parent; } /** * Name of the path element, i.e. name of the last (tail) property in the chain */ public @Nonnull String getName() { return name; } /** * Type of the path, i.e. type of the last (tail) property in the chain */ public @Nonnull Class<T> getType() { return type; } /** * Iterator over path elements, from {@code root} to {@code this}. * Contains at lest one path element — {@code this}, in case of * {@code this.isRoot()}. */ @Override public @Nonnull Iterator<BeanPath<?>> iterator() { return toCollection(new LinkedList<BeanPath<?>>()).iterator(); } private Collection<BeanPath<?>> toCollection(Collection<BeanPath<?>> collection) { if (hasParent()) { parent.toCollection(collection); } collection.add(this); return collection; } /** * Representation of the path in well familiar dot-delimited notation, * e.g. {@code foo.bar.baz}. * <p/> * Nameless root is not included. For a root path it returns an empty string. */ public @Nonnull String toDotDelimitedString() { String dds = cachedDotDelimitedString; if (dds == null) { cachedDotDelimitedString = dds = toDotDelimitedString(new StringBuilder(32)).toString(); } return dds; } // Instances of BeanPath is immutable, // thus we can lazy compute and cache derived properties. // Furthermore, we do not need synchronization: // its ok if two threads compute it twice concurrently. private transient String cachedDotDelimitedString = null; private StringBuilder toDotDelimitedString(StringBuilder sb) { if (isRoot()) { return sb; } if (getRoot() != parent) { parent.toDotDelimitedString(sb); sb.append('.'); } sb.append(name); return sb; } /** * Whether two paths are equal, i.e. represents same property chain * on same root bean */ @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } final BeanPath that = (BeanPath) obj; return (this.parent == that.parent || (this.parent != null && that.parent != null && this.parent.equals(that.parent))) && this.name.equals(that.name) && this.type.equals(that.type); } /** * Hash code of the path, consistent with equality */ @Override public int hashCode() { int hc = cachedHashCode; if (hc == 0) { hc = (parent != null) ? parent.hashCode() : 0; hc = 31 * hc + name.hashCode(); hc = 31 * hc + type.hashCode(); cachedHashCode = hc; } return hc; } // Instances of BeanPath are immutable, // thus we can lazy compute and cache derived properties. // Furthermore, we do not need synchronization: // its ok if two threads compute it twice concurrently. private transient int cachedHashCode = 0; /** * String representation of the path, for debugging purposes. * Currently it looks like this: * <pre>{@code * <root>:TypeOfRoot//propertyOne:Type//someString:String * }</pre> * <p/> * But do not relay on it it is subject to change without notice! */ @Override public @Nonnull String toString() { String ts = cachedToString; if (ts == null) { cachedToString = ts = toString(new StringBuilder(32)).toString(); } return ts; } // Instances of BeanPath are immutable, // thus we can lazy compute and cache derived properties. // Furthermore, we do not need synchronization: // its ok if two threads compute it twice concurrently. private transient String cachedToString = null; private StringBuilder toString(StringBuilder sb) { if (hasParent()) { parent.toString(sb).append("/"); } sb.append(name).append(':').append(type.getSimpleName()); return sb; } }