/*
* Copyright (c) 2014 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.xnode;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.namespace.QName;
import com.evolveum.midpoint.prism.Visitor;
import com.evolveum.midpoint.util.DebugUtil;
import com.evolveum.midpoint.util.MiscUtil;
import com.evolveum.midpoint.util.PrettyPrinter;
import com.evolveum.midpoint.util.QNameUtil;
import com.evolveum.midpoint.util.exception.SchemaException;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.jetbrains.annotations.NotNull;
public class MapXNode extends XNode implements Map<QName,XNode>, Serializable {
// We want to maintain ordering, hence the List
private List<Entry> subnodes = new ArrayList<Entry>();
public int size() {
return subnodes.size();
}
public boolean isEmpty() {
return subnodes.isEmpty();
}
public boolean containsKey(Object key) {
if (!(key instanceof QName)) {
throw new IllegalArgumentException("Key must be QName, but it is "+key);
}
return findEntry((QName)key) != null;
}
public boolean containsValue(Object value) {
if (!(value instanceof XNode)) {
throw new IllegalArgumentException("Value must be XNode, but it is "+value);
}
return findEntry((XNode)value) != null;
}
public XNode get(Object key) {
if (!(key instanceof QName)) {
throw new IllegalArgumentException("Key must be QName, but it is "+key);
}
Entry entry = findEntry((QName)key);
if (entry == null) {
return null;
}
return entry.getValue();
}
public XNode put(Map.Entry<QName, XNode> entry) {
return put(entry.getKey(), entry.getValue());
}
public XNode put(QName key, XNode value) {
XNode previous = removeEntry(key);
subnodes.add(new Entry(key, value));
return previous;
}
public Entry putReturningEntry(QName key, XNode value) {
removeEntry(key);
Entry e = new Entry(key, value);
subnodes.add(e);
return e;
}
public XNode remove(Object key) {
if (!(key instanceof QName)) {
throw new IllegalArgumentException("Key must be QName, but it is "+key);
}
return removeEntry((QName)key);
}
public void putAll(Map<? extends QName, ? extends XNode> m) {
for (Map.Entry<?, ?> entry: m.entrySet()) {
put((QName)entry.getKey(), (XNode)entry.getValue());
}
}
public void clear() {
subnodes.clear();
}
public Set<QName> keySet() {
Set<QName> keySet = new HashSet<QName>();
for (Entry entry: subnodes) {
keySet.add(entry.getKey());
}
return keySet;
}
public Collection<XNode> values() {
Collection<XNode> values = new ArrayList<XNode>(subnodes.size());
for (Entry entry: subnodes) {
values.add(entry.getValue());
}
return values;
}
public java.util.Map.Entry<QName, XNode> getSingleSubEntry(String errorContext) throws SchemaException {
if (isEmpty()) {
return null;
}
if (size() > 1) {
throw new SchemaException("More than one element in " + errorContext +" : "+dumpKeyNames());
}
return subnodes.get(0);
}
public Entry getSingleEntryThatDoesNotMatch(QName... excludedKeys) throws SchemaException {
Entry found = null;
OUTER: for (Entry subentry: subnodes) {
for (QName excludedKey: excludedKeys) {
if (QNameUtil.match(subentry.getKey(), excludedKey)) {
continue OUTER;
}
}
if (found != null) {
throw new SchemaException("More than one extension subnode found under "+this+": "+found.getKey()+" and "+subentry.getKey());
} else {
found = subentry;
}
}
return found;
}
@NotNull
public Set<java.util.Map.Entry<QName, XNode>> entrySet() {
Set<java.util.Map.Entry<QName, XNode>> entries = new Set<Map.Entry<QName,XNode>>() {
@Override
public int size() {
return subnodes.size();
}
@Override
public boolean isEmpty() {
return subnodes.isEmpty();
}
@Override
public boolean contains(Object o) {
return subnodes.contains(o);
}
@Override
public Iterator<java.util.Map.Entry<QName, XNode>> iterator() {
return (Iterator)subnodes.iterator();
}
@Override
public Object[] toArray() {
return subnodes.toArray();
}
@Override
public <T> T[] toArray(T[] a) {
return subnodes.toArray(a);
}
@Override
public boolean add(java.util.Map.Entry<QName, XNode> e) {
throw new UnsupportedOperationException();
// put(e.getKey(), e.getValue());
// return true;
}
@Override
public boolean remove(Object o) {
throw new UnsupportedOperationException();
}
@Override
public boolean containsAll(Collection<?> c) {
return subnodes.containsAll(c);
}
@Override
public boolean addAll(Collection<? extends java.util.Map.Entry<QName, XNode>> c) {
throw new UnsupportedOperationException();
}
@Override
public boolean retainAll(Collection<?> c) {
throw new UnsupportedOperationException();
}
@Override
public boolean removeAll(Collection<?> c) {
throw new UnsupportedOperationException();
}
@Override
public void clear() {
throw new UnsupportedOperationException();
}
};
return entries;
}
public <T> T getParsedPrimitiveValue(QName key, QName typeName) throws SchemaException {
XNode xnode = get(key);
if (xnode == null) {
return null;
}
if (!(xnode instanceof PrimitiveXNode<?>)) {
throw new SchemaException("Expected that field "+key+" will be primitive, but it is "+xnode.getDesc());
}
PrimitiveXNode<T> xprim = (PrimitiveXNode<T>)xnode;
return xprim.getParsedValue(typeName, null); // TODO expected class
}
public void merge(MapXNode other) {
for (java.util.Map.Entry<QName, XNode> otherEntry: other.entrySet()) {
QName otherKey = otherEntry.getKey();
XNode otherValue = otherEntry.getValue();
merge (otherKey, otherValue);
}
}
public void merge(QName otherKey, XNode otherValue) {
XNode myValue = get(otherKey);
if (myValue == null) {
put(otherKey, otherValue);
} else {
ListXNode myList;
if (myValue instanceof ListXNode) {
myList = (ListXNode)myValue;
} else {
myList = new ListXNode();
myList.add(myValue);
put(otherKey, myList);
}
if (otherValue instanceof ListXNode) {
myList.addAll((ListXNode)otherValue);
} else {
myList.add(otherValue);
}
}
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
for (Entry subentry: subnodes) {
if (subentry.value != null) {
subentry.value.accept(visitor);
} else {
//throw new IllegalStateException("null value of key " + subentry.key + " in map: " + debugDump());
}
}
}
public boolean equals(Object o) {
if (!(o instanceof MapXNode)){
return false;
}
MapXNode other = (MapXNode) o;
return MiscUtil.unorderedCollectionEquals(this.entrySet(), other.entrySet());
}
public int hashCode() {
int result = 0xCAFEBABE;
for (XNode node : this.values()) {
if (node != null){
result = result ^ node.hashCode(); // using XOR instead of multiplying and adding in order to achieve commutativity
}
}
return result;
}
@Override
public String debugDump(int indent) {
StringBuilder sb = new StringBuilder();
DebugUtil.debugDumpMapMultiLine(sb, this, indent, true, dumpSuffix());
return sb.toString();
}
@Override
public String getDesc() {
return "map";
}
@Override
public String toString() {
return "XNode(map:"+subnodes.size()+" entries)";
}
private Entry findEntry(QName qname) {
for (Entry entry: subnodes) {
if (QNameUtil.match(qname,entry.getKey())) {
return entry;
}
}
return null;
}
private Entry findEntry(XNode xnode) {
for (Entry entry: subnodes) {
if (entry.getValue().equals(xnode)) {
return entry;
}
}
return null;
}
private XNode removeEntry(QName key) {
Iterator<Entry> iterator = subnodes.iterator();
while (iterator.hasNext()) {
Entry entry = iterator.next();
if (QNameUtil.match(key,entry.getKey())) {
iterator.remove();
return entry.getValue();
}
}
return null;
}
public String dumpKeyNames() {
StringBuilder sb = new StringBuilder();
Iterator<Entry> iterator = subnodes.iterator();
while (iterator.hasNext()) {
Entry entry = iterator.next();
sb.append(PrettyPrinter.prettyPrint(entry.getKey()));
if (iterator.hasNext()) {
sb.append(",");
}
}
return sb.toString();
}
public void qualifyKey(QName key, String newNamespace) {
for (Entry entry : subnodes) {
if (key.equals(entry.getKey())) {
entry.qualifyKey(newNamespace);
}
}
}
public XNode replace(QName key, XNode value) {
for (Entry entry : subnodes) {
if (entry.getKey().equals(key)) {
XNode previous = entry.getValue();
entry.setValue(value);
return previous;
}
}
return put(key, value);
}
public RootXNode getEntryAsRoot(@NotNull QName key) {
XNode xnode = get(key);
return xnode != null ? new RootXNode(key, xnode) : null;
}
private class Entry implements Map.Entry<QName, XNode>, Serializable {
private QName key;
private XNode value;
public Entry(QName key) {
super();
this.key = key;
}
public Entry(QName key, XNode value) {
super();
this.key = key;
this.value = value;
}
public void qualifyKey(String newNamespace) {
Validate.notNull(key, "Key is null");
if (StringUtils.isNotEmpty(key.getNamespaceURI())) {
throw new IllegalStateException("Cannot qualify already qualified key: " + key);
}
key = new QName(newNamespace, key.getLocalPart());
}
@Override
public QName getKey() {
return key;
}
@Override
public XNode getValue() {
return value;
}
@Override
public XNode setValue(XNode value) {
this.value = value;
return value;
}
@Override
public String toString() {
return "E(" + key + ": " + value + ")";
}
/**
* Compares two entries of the MapXNode.
*
* It is questionable whether to compare QNames exactly or approximately (using QNameUtil.match) here.
* For the time being, exact comparison was chosen. The immediate reason is to enable correct
* processing of diff on RawTypes (e.g. to make "debug edit" to be able to change from xyz to c:xyz
* in element names, see MID-1969).
*
* TODO: In the long run, we have to think out where exactly we want to use approximate matching of QNames.
* E.g. it is reasonable to use it only where "deployer input" is expected (e.g. import of data objects),
* not in the internals of midPoint.
*/
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Entry entry = (Entry) o;
if (key != null ? !key.equals(entry.key) : entry.key != null) return false;
if (value != null ? !value.equals(entry.value) : entry.value != null) return false;
return true;
}
@Override
public int hashCode() {
int result = key != null && key.getLocalPart() != null ? key.getLocalPart().hashCode() : 0;
result = 31 * result + (value != null ? value.hashCode() : 0);
return result;
}
}
}