/*
* 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;
import com.evolveum.midpoint.prism.delta.ItemDelta;
import com.evolveum.midpoint.prism.path.ItemPath;
import com.evolveum.midpoint.util.DebugDumpable;
import com.evolveum.midpoint.util.DebugUtil;
import com.evolveum.midpoint.util.MiscUtil;
import com.evolveum.midpoint.util.PrettyPrinter;
import com.evolveum.midpoint.util.exception.SchemaException;
import com.evolveum.midpoint.util.exception.SystemException;
import org.jetbrains.annotations.NotNull;
import javax.xml.namespace.QName;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.util.*;
import java.util.function.Function;
/**
* Item is a common abstraction of Property and PropertyContainer.
* <p/>
* This is supposed to be a superclass for all items. Items are things
* that can appear in property containers, which generally means only a property
* and property container itself. Therefore this is in fact superclass for those
* two definitions.
*
* @author Radovan Semancik
*/
public abstract class Item<V extends PrismValue, D extends ItemDefinition> implements Itemable, DebugDumpable, Visitable, PathVisitable, Serializable, Revivable {
private static final long serialVersionUID = 510000191615288733L;
// The object should basically work without definition and prismContext. This is the
// usual case when it is constructed "out of the blue", e.g. as a new JAXB object
// It may not work perfectly, but basic things should work
protected QName elementName;
protected PrismValue parent;
protected D definition;
@NotNull protected final List<V> values = new ArrayList<>();
private transient Map<String,Object> userData = new HashMap<>();;
protected boolean immutable;
protected boolean incomplete;
protected transient PrismContext prismContext; // beware, this one can easily be null
/**
* This is used for definition-less construction, e.g. in JAXB beans.
*
* The constructors should be used only occasionally (if used at all).
* Use the factory methods in the ResourceObjectDefintion instead.
*/
Item(QName elementName) {
super();
this.elementName = elementName;
}
Item(QName elementName, PrismContext prismContext) {
super();
this.elementName = elementName;
this.prismContext = prismContext;
}
/**
* The constructors should be used only occasionally (if used at all).
* Use the factory methods in the ResourceObjectDefintion instead.
*/
Item(QName elementName, D definition, PrismContext prismContext) {
super();
this.elementName = elementName;
this.definition = definition;
this.prismContext = prismContext;
}
/**
* Returns applicable property definition.
* <p/>
* May return null if no definition is applicable or the definition is not
* know.
*
* @return applicable property definition
*/
public D getDefinition() {
return definition;
}
public boolean hasCompleteDefinition() {
return getDefinition() != null;
}
/**
* Returns the name of the property.
* <p/>
* The name is a QName. It uniquely defines a property.
* <p/>
* The name may be null, but such a property will not work.
* <p/>
* The name is the QName of XML element in the XML representation.
*
* @return property name
*/
@Override
public QName getElementName() {
return elementName;
}
/**
* Sets the name of the property.
* <p/>
* The name is a QName. It uniquely defines a property.
* <p/>
* The name may be null, but such a property will not work.
* <p/>
* The name is the QName of XML element in the XML representation.
*
* @param elementName the name to set
*/
public void setElementName(QName elementName) {
checkMutability();
this.elementName = elementName;
}
/**
* Sets applicable property definition.
*
* @param definition the definition to set
*/
public void setDefinition(D definition) {
checkMutability();
checkDefinition(definition);
this.definition = definition;
}
/**
* Returns a display name for the property type.
* <p/>
* Returns null if the display name cannot be determined.
* <p/>
* The display name is fetched from the definition. If no definition
* (schema) is available, the display name will not be returned.
*
* @return display name for the property type
*/
public String getDisplayName() {
return getDefinition() == null ? null : getDefinition().getDisplayName();
}
/**
* Returns help message defined for the property type.
* <p/>
* Returns null if the help message cannot be determined.
* <p/>
* The help message is fetched from the definition. If no definition
* (schema) is available, the help message will not be returned.
*
* @return help message for the property type
*/
public String getHelp() {
return getDefinition() == null ? null : getDefinition().getHelp();
}
/**
* Flag that indicates incomplete item. If set to true then the
* values in this item are not complete. If this flag is true
* then it can be assumed that the object that this item represents
* has at least one value. This is a method how to indicate that
* the item really has some values, but are not here. This may
* be used for variety of purposes. It may indicate that the
* account has a password, but the password value is not revealed.
* This may indicate that a user has a photo, but the photo was not
* requested and therefore is not returned. This may be used to indicate
* that only part of the attribute values were returned from the search.
* And so on.
*/
public boolean isIncomplete() {
return incomplete;
}
public void setIncomplete(boolean incomplete) {
this.incomplete = incomplete;
}
@Override
public PrismContext getPrismContext() {
if (prismContext != null) {
return prismContext;
} else if (parent != null) {
return parent.getPrismContext();
} else {
return null;
}
}
// Primarily for testing
public PrismContext getPrismContextLocal() {
return prismContext;
}
public void setPrismContext(PrismContext prismContext) {
this.prismContext = prismContext;
}
public PrismValue getParent() {
return parent;
}
public void setParent(PrismValue parentValue) {
if (this.parent != null && parentValue != null && this.parent != parentValue) {
throw new IllegalStateException("Attempt to reset parent of item "+this+" from "+this.parent+" to "+parentValue);
}
// Immutability check can be skipped, as setting the parent doesn't alter this object.
// However, if existing parent itself is immutable, adding/removing its child item will cause the exception.
this.parent = parentValue;
}
public ItemPath getPath() {
if (parent == null) {
return new ItemPath(getElementName());
}
return parent.getPath().subPath(getElementName());
}
public Map<String, Object> getUserData() {
if (userData == null) {
userData = new HashMap<>();
}
if (immutable) {
return Collections.unmodifiableMap(userData); // TODO beware, objects in userData themselves are mutable
} else {
return userData;
}
}
public Object getUserData(String key) {
// TODO make returned data immutable (?)
return getUserData().get(key);
}
public void setUserData(String key, Object value) {
checkMutability();
getUserData().put(key, value);
}
@NotNull
public List<V> getValues() {
return values;
}
public V getValue(int index) {
if (index < 0) {
index = values.size() + index;
}
return values.get(index);
}
public abstract <X> X getRealValue();
public abstract <X> Collection<X> getRealValues();
public boolean hasValue(PrismValue value, boolean ignoreMetadata) {
return (findValue(value, ignoreMetadata) != null);
}
public boolean hasValue(PrismValue value) {
return hasValue(value, false);
}
public boolean hasRealValue(PrismValue value) {
return hasValue(value, true);
}
public boolean isSingleValue() {
// We are not sure about multiplicity if there is no definition or the definition is dynamic
if (getDefinition() != null) {
if (getDefinition().isMultiValue()) {
return false;
}
}
return values.size() <= 1;
}
/**
* Returns value that is equal or equivalent to the provided value.
* The returned value is an instance stored in this item, while the
* provided value argument may not be.
*/
public PrismValue findValue(PrismValue value, boolean ignoreMetadata) {
for (PrismValue myVal : getValues()) {
if (myVal.equalsComplex(value, ignoreMetadata, false)) {
return myVal;
}
}
return null;
}
/**
* Returns value that is previous to the specified value.
* Note that the order is semantically insignificant and this is used only
* for presentation consistency in order-sensitive formats such as XML or JSON.
*/
public PrismValue getPreviousValue(PrismValue value) {
PrismValue previousValue = null;
for (PrismValue myVal : getValues()) {
if (myVal == value) {
return previousValue;
}
previousValue = myVal;
}
throw new IllegalStateException("The value "+value+" is not any of "+this+" values, therefore cannot determine previous value");
}
/**
* Returns values that is following the specified value.
* Note that the order is semantically insignificant and this is used only
* for presentation consistency in order-sensitive formats such as XML or JSON.
*/
public PrismValue getNextValue(PrismValue value) {
Iterator<V> iterator = getValues().iterator();
while (iterator.hasNext()) {
PrismValue myVal = iterator.next();
if (myVal == value) {
if (iterator.hasNext()) {
return iterator.next();
} else {
return null;
}
}
}
throw new IllegalStateException("The value "+value+" is not any of "+this+" values, therefore cannot determine next value");
}
public Collection<V> getClonedValues() {
Collection<V> clonedValues = new ArrayList<V>(getValues().size());
for (V val: getValues()) {
clonedValues.add((V)val.clone());
}
return clonedValues;
}
public boolean contains(V value) {
return contains(value, false);
}
public boolean containsEquivalentValue(V value) {
return contains(value, true);
}
public boolean contains(V value, boolean ignoreMetadata, Comparator<V> comparator) {
if (comparator == null){
return contains(value, ignoreMetadata);
} else{
for (V myValue: getValues()) {
if (comparator.compare(myValue, value) == 0) {
return true;
}
}
}
return false;
}
public boolean contains(V value, boolean ignoreMetadata) {
for (V myValue: getValues()) {
if (myValue.equals(value, ignoreMetadata)) {
return true;
}
}
return false;
}
public boolean containsRealValue(V value) {
for (V myValue: getValues()) {
if (myValue.equalsRealValue(value)) {
return true;
}
}
return false;
}
public int size() {
return values.size();
}
public boolean addAll(Collection<V> newValues) throws SchemaException {
checkMutability(); // TODO consider weaker condition, like testing if there's a real change
boolean changed = false;
for (V val: newValues) {
if (add(val)) {
changed = true;
}
}
return changed;
}
public boolean add(@NotNull V newValue) throws SchemaException {
return add(newValue, true);
}
public boolean add(@NotNull V newValue, boolean checkUniqueness) throws SchemaException {
checkMutability();
if (newValue.getPrismContext() == null) {
newValue.setPrismContext(prismContext);
}
newValue.setParent(this);
if (checkUniqueness && containsEquivalentValue(newValue)) {
return false;
}
if (getDefinition() != null) {
newValue.applyDefinition(getDefinition(), false);
}
return values.add(newValue);
}
public boolean removeAll(Collection<V> newValues) {
checkMutability(); // TODO consider if there is real change
boolean changed = false;
for (V val: newValues) {
if (remove(val)) {
changed = true;
}
}
return changed;
}
public boolean remove(V newValue) {
checkMutability(); // TODO consider if there is real change
boolean changed = false;
Iterator<V> iterator = values.iterator();
while (iterator.hasNext()) {
V val = iterator.next();
// the same algorithm as when deleting the item value from delete delta
// TODO either make equalsRealValue return false if both PCVs have IDs and these IDs are different
// TODO or include a special test condition here; see MID-3828
if (val.representsSameValue(newValue, false) || val.equalsRealValue(newValue)) {
iterator.remove();
changed = true;
}
}
return changed;
}
public V remove(int index) {
checkMutability(); // TODO consider if there is real change
return values.remove(index);
}
public void replaceAll(Collection<V> newValues) throws SchemaException {
checkMutability(); // TODO consider if there is real change
values.clear();
addAll(newValues);
}
public void replace(V newValue) {
checkMutability(); // TODO consider if there is real change
values.clear();
newValue.setParent(this);
values.add(newValue);
}
public void clear() {
checkMutability(); // TODO consider if there is real change
values.clear();
}
public void normalize() {
checkMutability(); // TODO consider if there is real change
for (V value : values) {
value.normalize();
}
}
/**
* Merge all the values of other item to this item.
*/
public void merge(Item<V,D> otherItem) throws SchemaException {
for (V otherValue: otherItem.getValues()) {
if (!contains(otherValue)) {
add((V) otherValue.clone());
}
}
}
public abstract Object find(ItemPath path);
public abstract <IV extends PrismValue,ID extends ItemDefinition> PartiallyResolvedItem<IV,ID> findPartial(ItemPath path);
// We want this method to be consistent with property diff
public ItemDelta<V,D> diff(Item<V,D> other) {
return diff(other, true, false);
}
// We want this method to be consistent with property diff
public ItemDelta<V,D> diff(Item<V,D> other, boolean ignoreMetadata, boolean isLiteral) {
List<? extends ItemDelta> itemDeltas = new ArrayList<>();
diffInternal(other, itemDeltas, ignoreMetadata, isLiteral);
if (itemDeltas.isEmpty()) {
return null;
}
if (itemDeltas.size() > 1) {
throw new UnsupportedOperationException("Item multi-delta diff is not supported yet");
}
return itemDeltas.get(0);
}
protected void diffInternal(Item<V,D> other, Collection<? extends ItemDelta> deltas,
boolean ignoreMetadata, boolean isLiteral) {
ItemDelta delta = createDelta();
if (other == null) {
//other doesn't exist, so delta means delete all values
for (PrismValue value : getValues()) {
PrismValue valueClone = value.clone();
delta.addValueToDelete(valueClone);
}
} else {
// the other exists, this means that we need to compare the values one by one
Collection<PrismValue> outstandingOtheValues = new ArrayList<PrismValue>(other.getValues().size());
outstandingOtheValues.addAll(other.getValues());
for (PrismValue thisValue : getValues()) {
Iterator<PrismValue> iterator = outstandingOtheValues.iterator();
boolean found = false;
while (iterator.hasNext()) {
PrismValue otherValue = iterator.next();
if (thisValue.representsSameValue(otherValue, true) || delta == null) {
found = true;
// Matching IDs, look inside to figure out internal deltas
thisValue.diffMatchingRepresentation(otherValue, deltas,
ignoreMetadata, isLiteral);
// No need to process this value again
iterator.remove();
break;
// TODO either make equalsRealValue return false if both PCVs have IDs and these IDs are different
// TODO or include a special test condition here; see MID-3828
} else if (thisValue.equalsComplex(otherValue, ignoreMetadata, isLiteral)) {
found = true;
// same values. No delta
// No need to process this value again
iterator.remove();
break;
}
}
if (!found) {
// We have the value and the other does not, this is delete of the entire value
delta.addValueToDelete(thisValue.clone());
}
}
// outstandingOtheValues are those values that the other has and we could not
// match them to any of our values. These must be new values to add
for (PrismValue outstandingOtherValue : outstandingOtheValues) {
delta.addValueToAdd(outstandingOtherValue.clone());
}
// Some deltas may need to be polished a bit. E.g. transforming
// add/delete delta to a replace delta.
delta = fixupDelta(delta, other, ignoreMetadata);
}
if (delta != null && !delta.isEmpty()) {
((Collection)deltas).add(delta);
}
}
protected ItemDelta<V,D> fixupDelta(ItemDelta<V,D> delta, Item<V,D> other,
boolean ignoreMetadata) {
return delta;
}
/**
* Creates specific subclass of ItemDelta appropriate for type of item that this definition
* represents (e.g. PropertyDelta, ContainerDelta, ...)
*/
public abstract ItemDelta<V,D> createDelta();
public abstract ItemDelta<V,D> createDelta(ItemPath path);
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
for(PrismValue value: getValues()) {
value.accept(visitor);
}
}
@Override
public void accept(Visitor visitor, ItemPath path, boolean recursive) {
// This implementation is supposed to only work for non-hierarchical items, such as properties and references.
// hierarchical items must override it.
if (recursive) {
accept(visitor);
} else {
visitor.visit(this);
}
}
public void filterValues(Function<V, Boolean> function) {
Iterator<V> iterator = values.iterator();
while (iterator.hasNext()) {
Boolean keep = function.apply(iterator.next());
if (keep == null || !keep) {
iterator.remove();
}
}
}
public void applyDefinition(D definition) throws SchemaException {
applyDefinition(definition, true);
}
public void applyDefinition(D definition, boolean force) throws SchemaException {
checkMutability(); // TODO consider if there is real change
if (definition != null) {
checkDefinition(definition);
}
if (this.prismContext == null && definition != null) {
this.prismContext = definition.getPrismContext();
}
this.definition = definition;
for (PrismValue pval: getValues()) {
pval.applyDefinition(definition, force);
}
}
public void revive(PrismContext prismContext) throws SchemaException {
// it is necessary to do e.g. PolyString recomputation even if PrismContext is set
if (this.prismContext == null) {
this.prismContext = prismContext;
if (definition != null) {
definition.revive(prismContext);
}
}
for (V value: values) {
value.revive(prismContext);
}
}
public abstract Item clone();
protected void copyValues(Item clone) {
clone.elementName = this.elementName;
clone.definition = this.definition;
clone.prismContext = this.prismContext;
// Do not clone parent so the cloned item can be safely placed to
// another item
clone.parent = null;
clone.userData = MiscUtil.cloneMap(this.userData);
clone.incomplete = this.incomplete;
// Also do not copy 'immutable' flag so the clone is free to be modified
}
protected void propagateDeepCloneDefinition(boolean ultraDeep, D clonedDefinition) {
// nothing to do by default
}
public static <T extends Item> Collection<T> cloneCollection(Collection<T> items) {
Collection<T> clones = new ArrayList<T>(items.size());
for (T item: items) {
clones.add((T)item.clone());
}
return clones;
}
/**
* Sets all parents to null. This is good if the items are to be "transplanted" into a
* different Containerable.
*/
public static <T extends Item> Collection<T> resetParentCollection(Collection<T> items) {
for (T item: items) {
item.setParent(null);
}
return items;
}
public static <T extends Item> T createNewDefinitionlessItem(QName name, Class<T> type, PrismContext prismContext) {
T item = null;
try {
Constructor<T> constructor = type.getConstructor(QName.class);
item = constructor.newInstance(name);
if (prismContext != null) {
item.revive(prismContext);
}
} catch (Exception e) {
throw new SystemException("Error creating new definitionless "+type.getSimpleName()+": "+e.getClass().getName()+" "+e.getMessage(),e);
}
return item;
}
public void checkConsistence(boolean requireDefinitions, ConsistencyCheckScope scope) {
checkConsistenceInternal(this, requireDefinitions, false, scope);
}
public void checkConsistence(boolean requireDefinitions, boolean prohibitRaw) {
checkConsistenceInternal(this, requireDefinitions, prohibitRaw, ConsistencyCheckScope.THOROUGH);
}
public void checkConsistence(boolean requireDefinitions, boolean prohibitRaw, ConsistencyCheckScope scope) {
checkConsistenceInternal(this, requireDefinitions, prohibitRaw, scope);
}
public void checkConsistence() {
checkConsistenceInternal(this, false, false, ConsistencyCheckScope.THOROUGH);
}
public void checkConsistence(ConsistencyCheckScope scope) {
checkConsistenceInternal(this, false, false, scope);
}
public void checkConsistenceInternal(Itemable rootItem, boolean requireDefinitions, boolean prohibitRaw, ConsistencyCheckScope scope) {
ItemPath path = getPath();
if (elementName == null) {
throw new IllegalStateException("Item "+this+" has no name ("+path+" in "+rootItem+")");
}
if (definition != null) {
checkDefinition(definition);
} else if (requireDefinitions && !isRaw()) {
throw new IllegalStateException("No definition in item "+this+" ("+path+" in "+rootItem+")");
}
if (values != null) {
for(V val: values) {
if (prohibitRaw && val.isRaw()) {
throw new IllegalStateException("Raw value "+val+" in item "+this+" ("+path+" in "+rootItem+")");
}
if (val == null) {
throw new IllegalStateException("Null value in item "+this+" ("+path+" in "+rootItem+")");
}
if (val.getParent() == null) {
throw new IllegalStateException("Null parent for value "+val+" in item "+this+" ("+path+" in "+rootItem+")");
}
if (val.getParent() != this) {
throw new IllegalStateException("Wrong parent for value "+val+" in item "+this+" ("+path+" in "+rootItem+"), "+
"bad parent: " + val.getParent());
}
val.checkConsistenceInternal(rootItem, requireDefinitions, prohibitRaw, scope);
}
}
}
protected abstract void checkDefinition(D def);
public void assertDefinitions() throws SchemaException {
assertDefinitions("");
}
public void assertDefinitions(String sourceDescription) throws SchemaException {
assertDefinitions(false, sourceDescription);
}
public void assertDefinitions(boolean tolarateRawValues, String sourceDescription) throws SchemaException {
if (tolarateRawValues && isRaw()) {
return;
}
if (definition == null) {
throw new SchemaException("No definition in "+this+" in "+sourceDescription);
}
}
/**
* Returns true is all the values are raw.
*/
public boolean isRaw() {
for (V val: getValues()) {
if (!val.isRaw()) {
return false;
}
}
return true;
}
/**
* Returns true is at least one of the values is raw.
*/
public boolean hasRaw() {
for (V val: getValues()) {
if (val.isRaw()) {
return true;
}
}
return false;
}
public boolean isEmpty() {
return getValues().isEmpty();
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((elementName == null) ? 0 : elementName.hashCode());
result = prime * result + ((values == null) ? 0 : MiscUtil.unorderedCollectionHashcode(values));
return result;
}
public boolean equalsRealValue(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Item<?,?> other = (Item<?,?>) obj;
if (elementName == null) {
if (other.elementName != null)
return false;
} else if (!elementName.equals(other.elementName))
return false;
// Do not compare parent at all. This is not relevant.
if (values == null) {
if (other.values != null)
return false;
} else if (!equalsRealValues(this.values, other.values))
return false;
return true;
}
private boolean equalsRealValues(List<V> thisValue, List<?> otherValues) {
return MiscUtil.unorderedCollectionEquals(thisValue, otherValues,
(o1, o2) -> {
if (o1 instanceof PrismValue && o2 instanceof PrismValue) {
PrismValue v1 = (PrismValue)o1;
PrismValue v2 = (PrismValue)o2;
return v1.equalsRealValue(v2);
} else {
return false;
}
});
}
private boolean match(List<V> thisValue, List<?> otherValues) {
return MiscUtil.unorderedCollectionEquals(thisValue, otherValues,
(o1, o2) -> {
if (o1 instanceof PrismValue && o2 instanceof PrismValue) {
PrismValue v1 = (PrismValue)o1;
PrismValue v2 = (PrismValue)o2;
return v1.match(v2);
} else {
return false;
}
});
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Item<?,?> other = (Item<?,?>) obj;
if (definition == null) {
if (other.definition != null)
return false;
} else if (!definition.equals(other.definition))
return false;
if (elementName == null) {
if (other.elementName != null)
return false;
} else if (!elementName.equals(other.elementName))
return false;
if (incomplete != other.incomplete) {
return false;
}
// Do not compare parent at all. This is not relevant.
if (values == null) {
if (other.values != null)
return false;
} else if (!MiscUtil.unorderedCollectionEquals(this.values, other.values))
return false;
return true;
}
public boolean match(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Item<?,?> other = (Item<?,?>) obj;
if (definition == null) {
if (other.definition != null)
return false;
} else if (!definition.equals(other.definition))
return false;
if (elementName == null) {
if (other.elementName != null)
return false;
} else if (!elementName.equals(other.elementName))
return false;
if (incomplete != other.incomplete) {
return false;
}
// Do not compare parent at all. This is not relevant.
if (values == null) {
if (other.values != null)
return false;
} else if (!match(this.values, other.values))
return false;
return true;
}
@Override
public String toString() {
return getClass().getSimpleName() + "(" + PrettyPrinter.prettyPrint(getElementName()) + ")";
}
public String debugDump(int indent) {
StringBuilder sb = new StringBuilder();
DebugUtil.indentDebugDump(sb, indent);
if (DebugUtil.isDetailedDebugDump()) {
sb.append(getDebugDumpClassName()).append(": ");
}
sb.append(DebugUtil.formatElementName(getElementName()));
return sb.toString();
}
/**
* Return a human readable name of this class suitable for logs.
*/
protected String getDebugDumpClassName() {
return "Item";
}
protected void appendDebugDumpSuffix(StringBuilder sb) {
if (incomplete) {
sb.append(" (incomplete)");
}
}
public boolean isImmutable() {
return immutable;
}
public void setImmutable(boolean immutable) {
this.immutable = immutable;
for (V value : getValues()) {
value.setImmutable(immutable);
}
}
protected void checkMutability() {
if (immutable) {
throw new IllegalStateException("An attempt to modify an immutable item: " + toString());
}
}
}