cdc.util.enums.AbstractForestDynamicEnum Maven / Gradle / Ivy
package cdc.util.enums;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Predicate;
import cdc.util.lang.Checks;
import cdc.util.lang.FailureReaction;
import cdc.util.lang.ImplementationException;
import cdc.util.lang.NotFoundException;
import cdc.util.lang.Operators;
import cdc.util.lang.UnexpectedValueException;
/**
* Dynamic enumeration of values organized as a forest (several trees).
*
* Each value has a unique qualified name.
* This implementation supports these features:
*
* - {@link DagFeature#LOCKING}
*
- {@link DagFeature#CREATION} till locked.
*
- {@link DagFeature#REMOVAL} if allowed at creation time and till locked.
*
- {@link DagFeature#CONTENT_CHANGE} if allowed at creation time and till locked.
*
- {@link DagFeature#REPARENTING} if allowed at creation time and till locked.
*
*
* A typical implementation should look like this:
*
{@code
* public final class Foo extends AbstractForestDynamicEnum {
* public static final Support SUPPORT = support(Foo.class, Foo::new, Feature.RENAMING, ...);
*
* protected Foo(Foo parent,
* String value) {
* super(parent, value);
* }
* }
* }
*
* @author Damien Carbonne
* @param The dynamic enum concrete type.
*/
public abstract class AbstractForestDynamicEnum> implements ForestDynamicEnum, Comparable {
private String name;
private String qname;
private V parent;
final List children = new ArrayList<>();
/**
* Character used to separate local names in paths.
*/
public static final char SEPARATOR = '/';
protected AbstractForestDynamicEnum(V parent,
String name) {
Checks.isNotNullOrEmpty(name, "name");
this.parent = parent;
this.name = name;
if (parent == null) {
qname = name;
} else {
@SuppressWarnings("unchecked")
final V tmp = (V) this;
parent.children.add(tmp);
qname = parent.getQName() + SEPARATOR + name;
}
}
protected final void setParent(V parent) {
this.parent = parent;
}
@Override
public final V getParent() {
return parent;
}
@Override
public final List getChildren() {
return Collections.unmodifiableList(children);
}
protected final void setName(String name) {
this.name = name;
if (parent == null) {
qname = name;
} else {
qname = parent.getQName() + SEPARATOR + name;
}
}
/**
* @return The local name.
*/
@Override
public final String getName() {
return name;
}
/**
* @return The qualified name (path).
*/
@Override
public final String getQName() {
return qname;
}
@Override
public String toString() {
return getQName();
}
@Override
public int hashCode() {
return getQName().hashCode();
}
@Override
public boolean equals(Object obj) {
return this == obj;
}
@Override
public int compareTo(V o) {
return getQName().compareTo(o.getQName());
}
/**
* Support interface describing standard methods expected for a dynamic enum.
*
* @author Damien Carbonne
*
* @param The enum type.
*/
public static interface Support> extends DynamicEnumSupport {
public V findOrCreate(V parent,
String path);
public void setParent(V value,
V parent);
}
/**
* Interface used to create new instances of a dynamic enum.
*
* @author Damien Carbonne
*
* @param The dynamic enum type.
*/
@FunctionalInterface
public static interface Creator> {
public V create(V parent,
String name);
}
public static interface Modifier> {
public void setName(V value,
String name);
public void setParent(V value,
V parent);
public void addChild(V value,
V child);
public void removeChild(V value,
V child);
}
public static > Support support(Class cls,
Predicate nameValidator,
Creator creator,
Modifier modifier,
DagFeature... features) {
Checks.isNotNull(cls, "cls");
Checks.isNotNull(nameValidator, "nameValidator");
Checks.isNotNull(creator, "creator");
return new SupportImpl<>(cls,
nameValidator,
creator,
modifier,
features);
}
/**
* Creates a support instance.
*
* @param The dynamic enum type.
* @param cls The dynamic enum class.
* @param nameValidator The predicate to check names validity.
* @param creator The dynamic enum factory
* @param features The features to enable.
* @return A new instance of Support for {@code }
*/
protected static > Support support(Class cls,
Predicate nameValidator,
Creator creator,
DagFeature... features) {
Checks.isNotNull(cls, "cls");
Checks.isNotNull(nameValidator, "nameValidator");
Checks.isNotNull(creator, "creator");
final Modifier modifier = new Modifier() {
@Override
public void setName(V value,
String name) {
value.setName(name);
}
@Override
public void setParent(V value,
V parent) {
value.setParent(parent);
}
@Override
public void addChild(V value,
V child) {
value.children.add(child);
}
@Override
public void removeChild(V value,
V child) {
value.children.remove(child);
}
};
return support(cls,
nameValidator,
creator,
modifier,
features);
}
protected static > Support support(Class cls,
Creator creator,
DagFeature... features) {
Checks.isNotNull(cls, "cls");
Checks.isNotNull(creator, "creator");
return support(cls,
AbstractDynamicEnumSupport.DEFAULT_NAME_VALIDATOR,
creator,
features);
}
/**
* Support Implementation.
*
* @author Damien Carbonne
*
* @param The dynamic enum type.
*/
private static final class SupportImpl> extends AbstractDynamicEnumSupport implements Support {
/** Ordered list of roots. */
private final List roots = new ArrayList<>();
/** Ordered list of valid values. */
private final List validValues = new ArrayList<>();
/** Map from qnames to values. */
private final Map qnameToValue = new HashMap<>();
private final Creator creator;
private final Modifier modifier;
private static final Predicate POSSIBLE_FEATURES = e -> {
switch (e) {
case CREATION:
case LOCKING:
case REMOVAL:
case CONTENT_CHANGE:
case REPARENTING:
return true;
default:
throw new UnexpectedValueException(e);
}
};
private final Consumer refreshQName;
protected SupportImpl(Class cls,
Predicate nameValidator,
Creator creator,
Modifier modifier,
DagFeature... features) {
super(cls,
nameValidator,
Checks.areAccepted(POSSIBLE_FEATURES, "features", features));
this.creator = creator;
this.modifier = modifier;
if (modifier == null) {
for (final DagFeature feature : features) {
if (feature == DagFeature.CONTENT_CHANGE || feature == DagFeature.REPARENTING) {
throw new IllegalArgumentException("Unexpected feature " + feature);
}
}
}
refreshQName = v -> {
qnameToValue.remove(v.getQName());
modifier.setName(v, v.getName());
qnameToValue.put(v.getQName(), v);
};
}
@Override
protected boolean isContained(V value) {
return validValues.contains(value);
}
@Override
public List getValues() {
return Collections.unmodifiableList(validValues);
}
@Override
public String getName(V value) {
return value == null ? null : value.getName();
}
@Override
public String getQName(V value) {
return value == null ? null : value.getQName();
}
@Override
public List getRoots() {
return Collections.unmodifiableList(roots);
}
@Override
public List getChildren(V value) {
return value.getChildren();
}
@Override
public List getParents(V value) {
return value.getParent() == null
? Collections.emptyList()
: Collections.unmodifiableList(Arrays.asList(value.getParent()));
}
@Override
public V valueOf(String qname,
FailureReaction reaction) {
return NotFoundException.onResult(qnameToValue.get(qname),
EnumType.unknownQName(qname),
logger,
reaction,
null);
}
@Override
public V findOrCreate(String qname) {
V value = qnameToValue.get(qname);
if (value == null) {
checkIsUnlocked();
final int pos = qname.lastIndexOf(SEPARATOR);
final V parent;
final String name;
if (pos < 0) {
parent = null;
name = qname;
} else {
parent = findOrCreate(qname.substring(0, pos));
name = qname.substring(pos + 1);
}
checkNameIsValid(name);
value = creator.create(parent, name);
if (!name.equals(value.getName()) || value.getParent() != parent) {
throw new ImplementationException(getValueClass().getCanonicalName()
+ " Unexpected name '" + value.getName() + "' under '" + parent + "'");
}
qnameToValue.put(qname, value);
validValues.add(value);
if (parent == null) {
roots.add(value);
}
fire(value, DagEventType.CREATED);
}
return value;
}
@Override
public V findOrCreate(V parent,
String path) {
if (parent == null) {
return findOrCreate(path);
} else {
return findOrCreate(parent.getQName() + SEPARATOR + path);
}
}
@Override
public void setParent(V value,
V parent) {
checkIsValid(value);
checkIsUnlocked();
checkIsSupported(DagFeature.REPARENTING);
checkIsNotOverOrEqual(value, parent);
if (value.getParent() != parent) {
if (value.getParent() != null) {
modifier.removeChild(value.getParent(), value);
} else {
roots.remove(value);
}
modifier.setParent(value, parent);
if (parent == null) {
roots.add(value);
} else {
modifier.addChild(parent, value);
}
// Update qualified names
iterateUnder(value, refreshQName);
fire(value, DagEventType.REPARENTED);
}
}
@Override
public void remove(V value) {
checkIsValid(value);
checkIsUnlocked();
checkIsSupported(DagFeature.REMOVAL);
// Remove children first
while (!value.getChildren().isEmpty()) {
final V last = value.getChildren().get(value.getChildren().size() - 1);
remove(last);
}
// Update parent or roots
if (value.getParent() != null) {
modifier.removeChild(value.getParent(), value);
} else {
roots.remove(value);
}
// Update caches
validValues.remove(value);
qnameToValue.remove(value.getQName());
fire(value, DagEventType.REMOVED);
}
@Override
public void setName(V value,
String name) {
checkIsValid(value);
checkIsUnlocked();
checkIsSupported(DagFeature.CONTENT_CHANGE);
checkNameIsValid(name);
checkHasNoSiblingNamed(value, name);
if (!value.getName().equals(name)) {
qnameToValue.remove(value.getQName());
modifier.setName(value, name);
qnameToValue.put(value.getQName(), value);
// Update children qualified names
for (final V child : value.getChildren()) {
iterateUnder(child, refreshQName);
}
fire(value, DagEventType.CONTENT_CHANGED);
}
}
@Override
public boolean isValid(V value) {
return value != null && qnameToValue.containsKey(value.getQName());
}
@Override
public boolean areEqual(V left,
V right) {
return Operators.equals(left, right);
}
@Override
public boolean isStrictlyOver(V left,
V right) {
return left != null && right != null && left.isStrictlyOver(right);
}
}
}