/*
* Beanfabrics Framework Copyright (C) by Michael Karneim, beanfabrics.org
* Use is subject to license terms. See license.txt.
*/
// TODO javadoc - remove this comment only when the class and all non-public
// methods and fields are documented
package org.beanfabrics;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;
/**
* Immutable representation of a path inside a presentation object model.
*
* @author Michael Karneim
*/
// TODO (mk) we need some methods for design time purpose, e.g.
// TODO (mk) make Path allow multiple results like XPath
// getting all path elements by using sample instances of (null)
// targets in EditorProperty objects
public class Path implements Iterable<String>, Serializable {
public static final String THIS_PATH_ELEMENT = "this";
public static final String PATH_SEPARATOR = ".";
public static final char PATH_SEPARATOR_CHAR = '.';
/**
* the elements of this path. Do not modify it's content after creation.
*/
private final String[] elements;
private final String pathStr;
private final int hashCode;
/**
* Creates a new identity ("this") path.
*/
public Path() {
this(THIS_PATH_ELEMENT);
}
/**
* Creates a new path from the given path string.
*
* @param pathStr a string of path elements delimited by the dot "."
* character.
* @throws IllegalArgumentException
*/
public Path(String pathStr)
throws IllegalArgumentException {
if (pathStr == null || pathStr == "") {
throw new IllegalArgumentException("pathStr==null");
}
StringTokenizer st = new StringTokenizer(pathStr, PATH_SEPARATOR);
int len = st.countTokens();
if (len == 0) {
throw new IllegalArgumentException("pathStr must contain at least one element but was '" + pathStr + "'");
}
List<String> elementsList = new LinkedList<String>();
int index = 0;
while (st.hasMoreTokens()) {
String token = st.nextToken();
if (THIS_PATH_ELEMENT.equals(token)) {
if (index == 0) {
// skip "this"
} else {
throw new IllegalArgumentException("only first element of path can be be 'this'");
}
} else {
elementsList.add(token);
}
index++;
}
if (pathStr.endsWith(PATH_SEPARATOR)) {
// TODO (mk) why is this allowed? Why adding an empty element? We should throw an exception instead!?!
elementsList.add(""); // add empty element to the end
}
this.elements = elementsList.toArray(new String[elementsList.size()]);
this.pathStr = pathStr;
this.hashCode = calculateHashCode(elements);
}
private Path(String[] elements, String pathStr) {
if (elements == null) {
throw new IllegalArgumentException("elements==null");
}
this.elements = elements;
this.pathStr = pathStr;
this.hashCode = calculateHashCode(elements);
}
/**
* Returns the path to the parent node of this path.
*
* @return the path to the parent node of this path
*/
public Path getParent() {
if (this.length() > 0) {
return getSubPath(0, this.length() - 1);
} else {
return null;
}
}
/**
* Returns a new path that is a subpath of this path. The subpath begins
* with the element at the specified <code>fromPosition</code> and extends
* to the end of this path. <pM>
*
* @param fromPosition returned path's first character's position in this
* path
* @return a new path that is a subpath of this path
* @throws IllegalArgumentException
*/
public Path getSubPath(int fromPosition)
throws IllegalArgumentException {
int length = this.length() - fromPosition;
return this.getSubPath(fromPosition, length);
}
/**
* Returns a new path that is a subpath of this path. The subpath begins at
* the specified <code>fromPosition</code> and the length of the subpath is
* <code>length</code>.
*
* @param fromPosition returned path's first character's position in this
* path
* @param length returned path's number of characters
* @return a new path that is a subpath of this path
* @throws IllegalArgumentException thrown if <code>length < 0</code> or
* <code>fromPosition + length > length</code>
*/
public Path getSubPath(int fromPosition, int length)
throws IllegalArgumentException {
if (length < 0) {
throw new IllegalArgumentException("length must not be less than 0 but was " + length);
}
if (fromPosition + length > length()) {
throw new IllegalArgumentException("length must not be greater than " + (fromPosition + length()) + " but was " + length);
}
String[] newElements = new String[length];
System.arraycopy(elements, fromPosition, newElements, 0, length);
boolean prefixWithTHIS = fromPosition == 0 && this.pathStr.startsWith(THIS_PATH_ELEMENT) && this.length() > 0;
String newPathStr = toPathString(newElements, prefixWithTHIS);
return new Path(newElements, newPathStr);
}
/**
* Returns the length of this path. The length is the distance from the root
* node to the target node. Therefor the length of the identity ("this")
* path is 0.
*
* @return the length of this path
*/
public int length() {
return elements.length;
}
/**
* Returns an iterator over this path's elements.
*/
public Iterator<String> iterator() {
return new Iterator<String>() {
final String[] elements = Path.this.elements;
int nextIndex = 0;
public boolean hasNext() {
return nextIndex < elements.length;
}
public String next() {
if (hasNext() == false) {
throw new NoSuchElementException();
}
return elements[nextIndex++];
}
public void remove() {
throw new UnsupportedOperationException("remove is not supported by this iterator");
}
};
}
/**
* Returns a collection of this path's elements.
*
* @return a collection of this path's elements
*/
public Collection<String> getElements() {
List<String> result = Arrays.asList(this.elements);
return result;
}
/**
* Returns the element at the specified index.
*
* @param index
* @return
*/
public String getElement(int index) {
return this.elements[index];
}
/**
* Returns the last element of this path or <code>null</code> if there are
* no elements.
*
* @return the last element of this path or <code>null</code> if there are
* no elements
*/
public String getLastElement() {
if (elements.length == 0) {
return null;
} else {
return elements[elements.length - 1];
}
}
/**
* Returns the canonical String representation of this path.
*
* @return the canonical String representation of this path
*/
public String toString() {
return this.getPathString();
}
/**
* Returns the canonical String representation of this path.
*
* @return the canonical String representation of this path
*/
public String getPathString() {
return this.pathStr;
}
/**
* Returns the hash code of this path.
*/
public int hashCode() {
return this.hashCode;
}
/**
* Indicates whether some other object is "equal to" this path.
*
* @return <code>true</code> if the obj argument is a {@link Path} and has
* the equal elements as this path
*/
public boolean equals(Object obj) {
if (obj == null || obj.getClass().equals(this.getClass()) == false) {
return false;
} else {
Path otherPath = (Path)obj;
if (this.hashCode != otherPath.hashCode || this.elements.length != otherPath.elements.length) {
return false;
}
for (int i = 0; i < elements.length; ++i) {
if (this.elements[i].equals(otherPath.elements[i]) == false) {
return false;
}
}
return true;
}
}
// STATIC METHODS //
/**
* Returns a Path object that represents a path defined by the given path
* string, or <code>null</code> if the path string is <code>null</code>.
*
* @return a Path object that represents a path defined by the given path
* string
*/
public static Path parse(String pathStr) {
if (pathStr == null || pathStr.trim().length() == 0) {
return null;
}
return new Path(pathStr);
}
/**
* Returns the canonical path string of the given path object, or
* <code>null</code> if the path object is <code>null</code>.
*
* @param path
* @return the canonical path string of the given path object
*/
public static String getPathString(Path path) {
if (path == null) {
return null;
} else {
return path.pathStr;
}
}
/**
* Concatenates the given path objects to a new path.
*
* @param paths
* @return the concatenated path
*/
public static Path concat(Path... paths) {
if (paths == null) {
throw new IllegalArgumentException("paths==null");
}
if (paths.length == 0) {
return null;
}
Path first = paths[0];
boolean prefixWithTHIS = first == null || first.pathStr.startsWith(THIS_PATH_ELEMENT);
List<String> elements = new LinkedList<String>();
for (Path p : paths) {
if (p != null) {
elements.addAll(p.getElements());
}
}
String[] elemArr = elements.toArray(new String[elements.size()]);
String pathStr = toPathString(elemArr, prefixWithTHIS);
return new Path(elemArr, pathStr);
}
/**
* Concatenates the given strings to a path string delimited by dot '.'
* characters.
*
* @param listOfStrings
* @return the concatenated strings delimited by dot characters
*/
public static String toPathString(List<String> listOfStrings) {
if (listOfStrings == null) {
throw new IllegalArgumentException("pathElements==null");
}
return toPathString((String[])listOfStrings.toArray(new String[listOfStrings.size()]));
}
/**
* Concatenates the given strings to a path string delimited by dot '.'
* characters.
*
* @param strings
* @return the concatenated strings delimited by dot characters
*/
public static String toPathString(String[] strings) {
return toPathString(strings, true);
}
/**
* Concatenates the given strings to a path string delimited by dot '.'
* characters.
*
* @param strings
* @param alwaysPrefixWithTHIS defines that the result string has to start
* with the identity ("this") element.
* @return the concatenated strings delimited by dot characters
*/
private static String toPathString(String[] strings, boolean alwaysPrefixWithTHIS) {
if (strings == null) {
throw new IllegalArgumentException("elements==null");
}
StringBuilder sb = new StringBuilder();
if (strings.length == 0) {
return THIS_PATH_ELEMENT;
}
for (int i = 0; i < strings.length; ++i) {
if (sb.length() > 0) {
sb.append(PATH_SEPARATOR);
} else if (alwaysPrefixWithTHIS) {
sb.append(THIS_PATH_ELEMENT).append(PATH_SEPARATOR);
}
sb.append(strings[i]);
}
return sb.toString();
}
private static int calculateHashCode(String[] elements) {
return toPathString(elements).hashCode();
}
}