/*
* Copyright (c) 2010-2017 Evolveum
*
* 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.evolveum.midpoint.prism.path;
import com.evolveum.midpoint.util.QNameUtil;
import com.evolveum.prism.xml.ns._public.types_3.ItemPathType;
import org.apache.commons.lang.Validate;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.xml.namespace.QName;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* @author semancik
*
*/
public class ItemPath implements Serializable, Cloneable {
@Deprecated // use ItemPathType.COMPLEX_TYPE
public static final QName XSD_TYPE = ItemPathType.COMPLEX_TYPE;
public static final ItemPath EMPTY_PATH = new ItemPath();
private List<ItemPathSegment> segments;
private Map<String, String> namespaceMap;
public void setNamespaceMap(Map<String, String> namespaceMap) {
this.namespaceMap = namespaceMap;
}
public Map<String, String> getNamespaceMap() {
return namespaceMap;
}
public ItemPath() {
segments = new ArrayList<>(0);
}
public ItemPath(QName... qnames) {
this.segments = new ArrayList<>(qnames.length);
for (QName qname : qnames) {
add(qname);
}
}
public ItemPath(String... names) {
this.segments = new ArrayList<>(names.length);
for (String name : names) {
add(stringToQName(name));
}
}
public ItemPath(Object... namesOrIds) {
this.segments = new ArrayList<>(namesOrIds.length);
for (Object nameOrId : namesOrIds) {
if (nameOrId instanceof QName) {
add((QName) nameOrId);
} else if (nameOrId instanceof String) {
add(stringToQName((String) nameOrId));
} else if (nameOrId instanceof Long) {
this.segments.add(new IdItemPathSegment((Long) nameOrId));
} else if (nameOrId instanceof Integer) {
this.segments.add(new IdItemPathSegment(((Integer) nameOrId).longValue()));
} else {
throw new IllegalArgumentException("Invalid item path segment value: " + nameOrId);
}
}
}
private QName stringToQName(String name) {
Validate.notNull(name, "name");
if (ParentPathSegment.SYMBOL.equals(name)) {
return ParentPathSegment.QNAME;
} else if (ObjectReferencePathSegment.SYMBOL.equals(name)) {
return ObjectReferencePathSegment.QNAME;
} else if (IdentifierPathSegment.SYMBOL.equals(name)) {
return IdentifierPathSegment.QNAME;
} else {
return new QName(name);
}
}
public ItemPath(ItemPath parentPath, QName subName) {
this.segments = new ArrayList<>(parentPath.segments.size()+1);
segments.addAll(parentPath.segments);
add(subName);
}
public ItemPath(ItemPath parentPath, ItemPath childPath) {
this.segments = new ArrayList<>(parentPath.segments.size()+childPath.segments.size());
segments.addAll(parentPath.segments);
segments.addAll(childPath.segments);
}
public ItemPath(List<ItemPathSegment> segments) {
this.segments = new ArrayList<>(segments.size());
this.segments.addAll(segments);
}
public ItemPath(List<ItemPathSegment> segments, ItemPathSegment subSegment) {
this.segments = new ArrayList<>(segments.size()+1);
this.segments.addAll(segments);
this.segments.add(subSegment);
}
public ItemPath(List<ItemPathSegment> segments, QName subName) {
this.segments = new ArrayList<>(segments.size()+1);
this.segments.addAll(segments);
add(subName);
}
public ItemPath(List<ItemPathSegment> segments, List<ItemPathSegment> additionalSegments) {
this.segments = new ArrayList<>(segments.size()+additionalSegments.size());
this.segments.addAll(segments);
this.segments.addAll(additionalSegments);
}
public ItemPath(ItemPathSegment... segments) {
this.segments = new ArrayList<>(segments.length);
Collections.addAll(this.segments, segments);
}
public ItemPath(ItemPath parentPath, ItemPathSegment subSegment) {
this.segments = new ArrayList<>(parentPath.segments.size() + 1);
this.segments.addAll(parentPath.segments);
this.segments.add(subSegment);
}
public ItemPath subPath(QName subName) {
return new ItemPath(segments, subName);
}
public ItemPath subPath(Object... components) {
return new ItemPath(segments, new ItemPath(components).segments);
}
public ItemPath subPath(Long id) {
return subPath(new IdItemPathSegment(id));
}
public ItemPath subPath(ItemPathSegment subSegment) {
return new ItemPath(segments, subSegment);
}
public ItemPath subPath(ItemPath subPath) {
ItemPath newPath = new ItemPath(segments);
newPath.segments.addAll(subPath.getSegments());
return newPath;
}
/**
* Null-proof static version.
*/
public static ItemPath subPath(ItemPath prefix, ItemPathSegment subSegment) {
if (prefix == null && subSegment == null) {
return EMPTY_PATH;
}
if (prefix == null) {
return new ItemPath(subSegment);
}
return prefix.subPath(subSegment);
}
private void add(QName qname) {
this.segments.add(createSegment(qname, false));
}
public static ItemPathSegment createSegment(QName qname, boolean variable) {
if (ParentPathSegment.QNAME.equals(qname)) {
return new ParentPathSegment();
} else if (ObjectReferencePathSegment.QNAME.equals(qname)) {
return new ObjectReferencePathSegment();
} else if (IdentifierPathSegment.QNAME.equals(qname)) {
return new IdentifierPathSegment();
} else {
return new NameItemPathSegment(qname, variable);
}
}
public List<ItemPathSegment> getSegments() {
return segments;
}
public ItemPathSegment first() {
if (segments.size() == 0) {
return null;
}
return segments.get(0);
}
@NotNull
public ItemPath rest() {
return tail();
}
public NameItemPathSegment lastNamed() {
for (int i = segments.size()-1; i >= 0; i--) {
if (segments.get(i) instanceof NameItemPathSegment) {
return (NameItemPathSegment) segments.get(i);
}
}
return null;
}
@Nullable
public ItemPathSegment last() {
if (segments.size() == 0) {
return null;
}
return segments.get(segments.size()-1);
}
/**
* Returns first segment in a form of path.
*/
public ItemPath head() {
return new ItemPath(first());
}
/**
* Returns path containing all segments except the first N.
*/
@NotNull
public ItemPath tail(int n) {
if (segments.size() < n) {
return EMPTY_PATH;
}
return new ItemPath(segments.subList(n, segments.size()));
}
@NotNull
public ItemPath tail() {
return tail(1);
}
/**
* Returns a path containing all segments except the last one.
*/
@NotNull
public ItemPath allExceptLast() {
if (segments.size() == 0) {
return EMPTY_PATH;
}
return new ItemPath(segments.subList(0, segments.size()-1));
}
/**
* Returns a path containing all segments up to (and not including) the last one.
*/
public ItemPath allUpToLastNamed() {
for (int i = segments.size()-1; i >= 0; i--) {
if (segments.get(i) instanceof NameItemPathSegment) {
return new ItemPath(segments.subList(0, i));
}
}
return EMPTY_PATH;
}
/**
* Returns a path containing all segments up to (not including) the specified one;
* counted from backwards.
* If the segment is not present, returns empty path.
*/
@SuppressWarnings("unused")
public ItemPath allUpTo(ItemPathSegment segment) {
int i = segments.lastIndexOf(segment);
if (i < 0) {
return EMPTY_PATH;
} else {
return new ItemPath(segments.subList(0, i));
}
}
/**
* Returns a path containing all segments up to (including) the specified one;
* counted from backwards.
* If the segment is not present, returns empty path.
*/
public ItemPath allUpToIncluding(ItemPathSegment segment) {
int i = segments.lastIndexOf(segment);
if (i < 0) {
return EMPTY_PATH;
} else {
return allUpToIncluding(i);
}
}
public ItemPath allUpToIncluding(int i) {
return new ItemPath(segments.subList(0, i+1));
}
public int size() {
return segments.size();
}
public boolean isEmpty() {
return segments.isEmpty();
}
/**
* Makes the path "normal" by inserting null Id segments where they were omitted.
*/
public ItemPath normalize() {
ItemPath normalizedPath = new ItemPath();
ItemPathSegment lastSegment = null;
Iterator<ItemPathSegment> iterator = segments.iterator();
while (iterator.hasNext()) {
ItemPathSegment origSegment = iterator.next();
if (lastSegment != null && !(lastSegment instanceof IdItemPathSegment) &&
!(origSegment instanceof IdItemPathSegment)) {
normalizedPath.segments.add(new IdItemPathSegment());
}
normalizedPath.segments.add(origSegment);
lastSegment = origSegment;
}
if (lastSegment != null && !(lastSegment instanceof IdItemPathSegment) &&
// Make sure we do not insert the Id segment as a last one. That is not correct and it would spoil comparing paths
iterator.hasNext()) {
normalizedPath.segments.add(new IdItemPathSegment());
}
return normalizedPath;
}
public CompareResult compareComplex(ItemPath otherPath) {
ItemPath thisNormalized = this.normalize();
ItemPath otherNormalized = otherPath == null ? EMPTY_PATH : otherPath.normalize();
int i = 0;
while (i < thisNormalized.segments.size() && i < otherNormalized.segments.size()) {
ItemPathSegment thisSegment = thisNormalized.segments.get(i);
ItemPathSegment otherSegment = otherNormalized.segments.get(i);
if (!thisSegment.equivalent(otherSegment)) {
return CompareResult.NO_RELATION;
}
i++;
}
if (i < thisNormalized.size()) {
return CompareResult.SUPERPATH;
}
if (i < otherNormalized.size()) {
return CompareResult.SUBPATH;
}
return CompareResult.EQUIVALENT;
}
public static boolean containsEquivalent(Collection<ItemPath> paths, ItemPath pathToBeFound) {
for (ItemPath path : paths) {
if (path.equivalent(pathToBeFound)) {
return true;
}
}
return false;
}
public static boolean containsSubpathOrEquivalent(Collection<ItemPath> paths, ItemPath pathToBeFound) {
for (ItemPath path : paths) {
CompareResult r = pathToBeFound.compareComplex(path);
if (r == CompareResult.SUBPATH || r == CompareResult.EQUIVALENT) {
return true;
}
}
return false;
}
public ItemPath namedSegmentsOnly() {
ItemPath rv = new ItemPath();
for (ItemPathSegment segment : segments) {
if (segment instanceof NameItemPathSegment) {
rv.add(((NameItemPathSegment) segment).getName());
}
}
return rv;
}
public static boolean isNullOrEmpty(ItemPath itemPath) {
return itemPath == null || itemPath.isEmpty();
}
@SuppressWarnings("unused")
public static boolean containsSingleNameSegment(ItemPath path) {
return path != null && path.size() == 1 && path.first() instanceof NameItemPathSegment;
}
public boolean startsWith(Class<? extends ItemPathSegment> clazz) {
return !isEmpty() && clazz.isAssignableFrom(first().getClass());
}
public boolean startsWith(ItemPath other) {
return other == null || other.isSubPathOrEquivalent(this);
}
public boolean startsWithName(QName name) {
return !isEmpty()
&& startsWith(NameItemPathSegment.class)
&& QNameUtil.match(name, ((NameItemPathSegment) first()).getName());
}
public boolean startsWithVariable() {
return !isEmpty() && first().isVariable();
}
public ItemPath stripVariableSegment() {
return startsWithVariable() ? rest() : this;
}
public QName asSingleName() {
if (size() == 1 && startsWith(NameItemPathSegment.class)) {
return ((NameItemPathSegment) first()).getName();
} else {
return null;
}
}
public static QName asSingleName(ItemPath path) {
return path != null ? path.asSingleName() : null;
}
public static ItemPath[] asPathArray(QName... names) {
ItemPath[] paths = new ItemPath[names.length];
int i = 0;
for (QName name : names) {
paths[i++] = new ItemPath(name);
}
return paths;
}
public ItemPath append(QName childName) {
return new ItemPath(this, childName);
}
public ItemPath append(ItemPath childPath) {
return new ItemPath(this, childPath);
}
@NotNull
public static List<ItemPath> fromStringList(List<String> pathsAsStrings) {
List<ItemPath> rv = new ArrayList<>();
if (pathsAsStrings != null) {
for (String pathAsString : pathsAsStrings) {
rv.add(new ItemPathType(pathAsString).getItemPath());
}
}
return rv;
}
public enum CompareResult {
EQUIVALENT, SUPERPATH, SUBPATH, NO_RELATION
}
public boolean isSubPath(ItemPath otherPath) {
return compareComplex(otherPath) == CompareResult.SUBPATH;
}
public boolean isSuperPath(ItemPath otherPath) {
return compareComplex(otherPath) == CompareResult.SUPERPATH;
}
public boolean isSubPathOrEquivalent(ItemPath otherPath) {
CompareResult result = compareComplex(otherPath);
return result == CompareResult.SUBPATH || result == CompareResult.EQUIVALENT;
}
/**
* Compares two paths semantically.
*/
public boolean equivalent(ItemPath otherPath) {
return compareComplex(otherPath) == CompareResult.EQUIVALENT;
}
public ItemPath substract(ItemPath otherPath) {
// return remainder(otherPath); // the code seems to be equivalent to the one of remainder()
ItemPath thisNormalized = this.normalize();
ItemPath otherNormalized = otherPath.normalize();
if (thisNormalized.size() < otherNormalized.size()) {
throw new IllegalArgumentException("Cannot substract path '"+otherPath+"' from '"+this+"' because it is not a subset");
}
int i = 0;
while (i < otherNormalized.segments.size()) {
ItemPathSegment thisSegment = thisNormalized.segments.get(i);
ItemPathSegment otherSegment = otherNormalized.segments.get(i);
if (!thisSegment.equivalent(otherSegment)) {
throw new IllegalArgumentException("Cannot subtract segment '"+otherSegment+"' from path '"+this+
"' because it does not contain corresponding segment; it has '"+thisSegment+"' instead.");
}
i++;
}
List<ItemPathSegment> substractSegments = thisNormalized.segments.subList(i, thisNormalized.segments.size());
return new ItemPath(substractSegments);
}
/**
* Returns the remainder of "this" path after passing all segments from the other path.
* (I.e. this path must begin with the content of the other path. Throws an exception when
* it is not the case.)
*/
public ItemPath remainder(ItemPath otherPath) {
ItemPath thisNormalized = this.normalize();
ItemPath otherNormalized = otherPath.normalize();
if (thisNormalized.size() < otherNormalized.size()) {
throw new IllegalArgumentException("Cannot compute remainder of path '"+this+"' after '"+otherPath+"' because this path is not a superset");
}
int i = 0;
while (i < otherNormalized.segments.size()) {
ItemPathSegment thisSegment = thisNormalized.segments.get(i);
ItemPathSegment otherSegment = otherNormalized.segments.get(i);
if (!thisSegment.equivalent(otherSegment)) {
throw new IllegalArgumentException("Cannot subtract segment '"+otherSegment+"' from path '"+this+
"' because it does not contain corresponding segment; it has '"+thisSegment+"' instead.");
}
i++;
}
List<ItemPathSegment> substractSegments = thisNormalized.segments.subList(i, thisNormalized.segments.size());
return new ItemPath(substractSegments);
}
/**
* Convenience static method with checks
* @throws IllegalArgumentException If the argument is an item path segment other than a named one
*/
public static QName getName(ItemPathSegment segment) {
if (segment == null) {
return null;
}
if (!(segment instanceof NameItemPathSegment)) {
throw new IllegalArgumentException("Unable to get name from non-name path segment "+segment);
}
return ((NameItemPathSegment)segment).getName();
}
public static IdItemPathSegment getFirstIdSegment(ItemPath itemPath) {
ItemPathSegment first = itemPath.first();
if (first instanceof IdItemPathSegment) {
return (IdItemPathSegment)first;
}
return null;
}
public static NameItemPathSegment getFirstNameSegment(ItemPath itemPath) {
if (itemPath == null) {
return null;
}
return itemPath.getFirstNameSegment();
}
public NameItemPathSegment getFirstNameSegment() {
ItemPathSegment first = first();
if (first instanceof NameItemPathSegment) {
return (NameItemPathSegment)first;
}
if (first instanceof IdItemPathSegment) {
return getFirstNameSegment(rest());
}
return null;
}
public static QName getFirstName(ItemPath itemPath) {
if (itemPath == null) {
return null;
}
return itemPath.getFirstName();
}
public QName getFirstName() {
NameItemPathSegment nameSegment = getFirstNameSegment();
if (nameSegment == null) {
return null;
}
return nameSegment.getName();
}
public static ItemPath pathRestStartingWithName(ItemPath path) {
ItemPathSegment pathSegment = path.first();
if (pathSegment instanceof NameItemPathSegment) {
return path;
} else if (pathSegment instanceof IdItemPathSegment) {
return path.rest();
} else {
throw new IllegalArgumentException("Unexpected path segment "+pathSegment);
}
}
public boolean containsName(QName name) {
for (ItemPathSegment segment: segments) {
if (segment instanceof NameItemPathSegment && ((NameItemPathSegment)segment).getName().equals(name)) {
return true;
}
}
return false;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
Iterator<ItemPathSegment> iterator = segments.iterator();
while (iterator.hasNext()) {
sb.append(iterator.next());
if (iterator.hasNext()) {
sb.append("/");
}
}
return sb.toString();
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((segments == null) ? 0 : segments.hashCode());
return result;
}
public boolean equals(Object obj, boolean exact) {
if (exact) {
return equals(obj);
} else {
return obj instanceof ItemPath && equivalent((ItemPath) obj);
}
}
/**
* More strict version of ItemPath comparison. Does not use any normalization
* nor approximate matching QNames via QNameUtil.match.
*
* For semantic-level comparison, please use equivalent(..) method.
*/
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
ItemPath other = (ItemPath) obj;
if (segments == null) {
if (other.segments != null)
return false;
} else if (!segments.equals(other.segments))
return false;
return true;
}
public ItemPath clone() {
ItemPath clone = new ItemPath();
for (ItemPathSegment segment : segments) {
clone.segments.add(segment.clone());
}
if (namespaceMap != null) {
clone.namespaceMap = new HashMap<>(namespaceMap);
}
return clone;
}
public static boolean containsSpecialSymbols(ItemPath path) {
return path != null && path.containsSpecialSymbols();
}
public boolean containsSpecialSymbols() {
return segments.stream().anyMatch(s -> s instanceof IdentifierPathSegment || s instanceof ReferencePathSegment);
}
public boolean containsSpecialSymbolsExceptParent() {
return segments.stream().anyMatch(s -> s instanceof IdentifierPathSegment || s instanceof ObjectReferencePathSegment);
}
public static void checkNoSpecialSymbols(ItemPath path) {
if (containsSpecialSymbols(path)) {
throw new IllegalStateException("Item path shouldn't contain special symbols but it does: " + path);
}
}
public static void checkNoSpecialSymbolsExceptParent(ItemPath path) {
if (path != null && path.containsSpecialSymbolsExceptParent()) {
throw new IllegalStateException("Item path shouldn't contain special symbols (except for parent) but it does: " + path);
}
}
public ItemPathType asItemPathType() {
return new ItemPathType(this);
}
}