cdc.util.enums.AbstractEnumMask Maven / Gradle / Ivy
package cdc.util.enums;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import cdc.util.function.Iterables;
import cdc.util.function.Predicates;
import cdc.util.lang.Checks;
import cdc.util.lang.CollectionsUtil;
/**
* Mask (set) of values belonging to an enum type.
*
* Objects of this class are immutable.
*
* A typical implementation should look like this:
*
{@code
* public final class FooMask extends AbstractEnumMask {
* public static final Support SUPPORT = support(FooMask.class, FooMask::new, FooType, Nullable.FALSE);
*
* private FooMask(Support support,
* Set values) {
* super(support, values);
* }
* }
* }
* For a standard enum, one can declare:
* {@code
* public final class FooMask extends AbstractEnumMask {
* public static final Support SUPPORT = support(FooMask.class, FooMask::new, Foo.class, Nullable.FALSE);
*
* private FooMask(Support support,
* Set values) {
* super(support, values);
* }
* }
* }
*
* @author Damien Carbonne
* @param The mask type.
* @param The enum type.
*/
public class AbstractEnumMask, V> {
/**
* The associated support class.
*/
protected final Support owner;
/**
* The mask values.
*/
protected final Set values;
protected AbstractEnumMask(Support support,
Set values) {
Checks.isNotNull(support, "support");
this.owner = support;
this.values = Collections.unmodifiableSet(values);
checkValues();
}
/**
* Support methods.
*
* @author Damien Carbonne
*
* @param The mask type.
* @param The enum type.
*/
public static interface Support {
/**
* @return The mask class.
*/
public Class getMaskClass();
/**
* @return The associated enum type.
*/
public EnumType getEnumType();
/**
* @return {@code true} if {@code null} is a valid mask value.
*/
public boolean isNullable();
/**
* @return The empty mask.
*/
public M empty();
/**
* @return The fill mask (at the time of call).
*/
public M full();
/**
* @return The empty mask.
*/
public M create();
/**
* Creates a mask with one value.
*
* @param value The value.
* @return The mask that contains {@code value}.
*/
public M create(V value);
/**
* Creates a mask from an array.
*
* @param values The values (possibly including {@code null}).
* @return The created mask.
*/
@SuppressWarnings("unchecked")
public M create(V... values);
/**
* Creates a mask from an iterable.
*
* @param values The values (possibly including {@code null}).
* @return The created mask.
*/
public M create(Iterable values);
/**
* Creates a mask from a predicate.
*
* @param predicate A predicate of values to include.
* @return The created mask.
*/
public M create(Predicate predicate);
/**
* Creates a full or empty mask.
*
* WARNING: Full is meaningful at the time of calling this constructor.
* It may become false if values change after this call.
*
* @param enabled If {@code true}, creates a full mask. An empty mask otherwise.
* @return The created mask.
*/
public M create(boolean enabled);
}
@FunctionalInterface
protected static interface Creator {
public M create(Support support,
Set values);
}
/**
* Creates a Support implementation.
*
* @param The mask type.
* @param The enum type.
* @param maskClass The mask class.
* @param creator The mask factory.
* @param enumType The enum type.
* @param nullable {@code Nullable.TRUE} if {@code null} is a valid value.
* @return A Support implementation.
*/
protected static , V> Support support(Class maskClass,
Creator creator,
EnumType enumType,
Nullable nullable) {
Checks.isNotNull(maskClass, "maskClass");
Checks.isNotNull(creator, "creator");
Checks.isNotNull(enumType, "enumType");
Checks.isNotNull(nullable, "nullable");
return new SupportImpl<>(maskClass, creator, enumType, nullable);
}
/**
* Creates a Support implementation for a standard enum.
*
* @param The mask type.
* @param The enum type.
* @param maskClass The mask class.
* @param creator The mask factory.
* @param enumClass The enum class.
* @param nullable {@code Nullable.TRUE} if {@code null} is a valid value.
* @return A Support implementation.
*/
protected static , V extends Enum> Support support(Class maskClass,
Creator creator,
Class enumClass,
Nullable nullable) {
Checks.isNotNull(maskClass, "maskClass");
Checks.isNotNull(creator, "creator");
Checks.isNotNull(nullable, "nullable");
return support(maskClass, creator, StandardEnumTypes.getEnumType(enumClass), nullable);
}
private void checkValue(V value) {
Checks.isTrue(value != null || isNullable(), "null is not supported");
}
private void checkOther(M other) {
Checks.isNotNull(other, "other");
Checks.isTrue(owner.isNullable() == other.owner.isNullable(), "Cannot mix masks with different nullable");
}
private void checkValues() {
Checks.isTrue(isNullable() || !values.contains((V) null), "null is not supported");
}
/**
* @return The enum type.
*/
public EnumType getEnumType() {
return owner.getEnumType();
}
/**
* @return {@code true} if this mask is a nullable mask.
*/
public boolean isNullable() {
return owner.isNullable();
}
/**
* @return {@code true} if this mask is empty.
*/
public boolean isEmpty() {
return values.isEmpty();
}
/**
*
* @return {@code true} if this mask is full.
* The result may change if underlying enum type is dynamic.
*/
public boolean isFull() {
if (values.size() != owner.getEnumType().getValues().size() + (isNullable() ? 1 : 0)) {
return false;
} else {
return this.equals(full());
}
}
/**
* @return The values.
*/
public Set getValues() {
return values;
}
/**
* Returns {@code true} if a value is contained.
*
* @param value The value.
* @return {@code true} if {@code value} is contained in this mask.
*/
public boolean isSet(V value) {
checkValue(value);
return values.contains(value);
}
/**
* Sets or clears a value.
*
* @param value The value.
* @param enabled If {@code true} sets the value, otherwise clears it.
* @return A new mask with {@code value} set or cleared.
*/
public M set(V value,
boolean enabled) {
checkValue(value);
return enabled ? set(value) : clear(value);
}
/**
* Returns a new mask augmented with a value.
*
* If {@code value} is already set, returns this mask.
*
* @param value The value.
* @return A new mask augmented with {@code value}.
*/
public M set(V value) {
checkValue(value);
if (isSet(value)) {
return owner.getMaskClass().cast(this);
} else {
final Set tmp = new HashSet<>();
tmp.addAll(getValues());
tmp.add(value);
return owner.create(tmp);
}
}
/**
* Returns a new mask reduced with a value.
*
* If {@code value} is already cleared, returns this mask.
*
* @param value The value.
* @return A new mask reduced with {@code value}.
*/
public M clear(V value) {
checkValue(value);
if (isSet(value)) {
final Set tmp = new HashSet<>();
tmp.addAll(getValues());
tmp.remove(value);
return owner.create(tmp);
} else {
return owner.getMaskClass().cast(this);
}
}
/**
* Returns an empty or full mask.
*
* The full mask may change if underlying enum type is dynamic.
*
* @param enabled If {@code true}, creates a full mask. An empty mask otherwise.
* @return An empty or full mask.
*/
public M setAll(boolean enabled) {
return owner.create(enabled);
}
/**
* @return An empty mask.
*/
public M empty() {
return setAll(false);
}
/**
* @return A full mask at the time of call.
* Result may change if underlying enum type is dynamic.
*/
public M full() {
return setAll(true);
}
/**
* Returns the intersection of this mask with another one.
*
* @param other The other mask.
* @return The intersection of this mask with {@code other}.
*/
public M and(M other) {
checkOther(other);
final Set tmp = new HashSet<>();
tmp.addAll(getValues());
tmp.retainAll(other.getValues());
return owner.create(tmp);
}
@SuppressWarnings("unchecked")
public M and(V... values) {
final M other = owner.create(values);
return and(other);
}
/**
* Returns the union of this mask with another one.
*
* @param other The other mask.
* @return The union of this mask with {@code other}.
*/
public M or(M other) {
checkOther(other);
final Set tmp = new HashSet<>();
tmp.addAll(getValues());
tmp.addAll(other.getValues());
return owner.create(tmp);
}
@SuppressWarnings("unchecked")
public M or(V... values) {
final M other = owner.create(values);
return or(other);
}
/**
* Returns the complement of this mask (at the time of calling this method).
*
* @return The complement of this mask (at the time of calling this method).
*/
public M not() {
final Set tmp = new HashSet<>();
tmp.addAll(owner.getEnumType().getValues());
if (owner.isNullable()) {
tmp.add(null);
}
tmp.removeAll(getValues());
return owner.create(tmp);
}
/**
* Returns {@code true} if all values of this mask are contained in another mask.
*
* @param other The other mask.
* @return {@code true} if all values of this mask are contained in {@code other}.
*/
public final boolean contains(M other) {
checkOther(other);
return and(other).equals(other);
}
@SafeVarargs
public final boolean contains(V... values) {
final M other = owner.create(values);
return contains(other);
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(owner.getMaskClass().isInstance(other))) {
return false;
}
final M o = owner.getMaskClass().cast(other);
return owner.getEnumType().equals(o.owner.getEnumType())
&& owner.isNullable() == o.owner.isNullable()
&& this.values.equals(o.values);
}
@Override
public int hashCode() {
return owner.getEnumType().hashCode()
+ (owner.isNullable() ? 1 : 0)
+ values.hashCode();
}
/**
* Returns a string representation of this mask.
*
* @param valueToString The function used to converter values (including {@code null}) to string.
* @param separator The separator to use between values.
* @return A string representation of this mask.
*/
public String toString(Function super V, String> valueToString,
String separator) {
final StringBuilder builder = new StringBuilder();
boolean first = true;
for (final V value : getEnumType().getValues()) {
if (isSet(value)) {
if (first) {
first = false;
} else {
builder.append(separator);
}
builder.append(valueToString.apply(value));
}
}
if (isNullable() && isSet(null)) {
if (!first) {
builder.append(separator);
}
builder.append(valueToString.apply(null));
}
return builder.toString();
}
@Override
public String toString() {
return toString(owner.getEnumType()::getQName, "|");
}
private static final class SupportImpl, V> implements Support {
public final Class cls;
private final Creator creator;
private final EnumType enumType;
private final Nullable nullable;
private final M empty;
public SupportImpl(Class cls,
Creator creator,
EnumType enumType,
Nullable nullable) {
this.cls = cls;
this.creator = creator;
this.enumType = enumType;
this.nullable = nullable;
this.empty = creator.create(this, Collections.emptySet());
}
@Override
public Class getMaskClass() {
return cls;
}
@Override
public EnumType getEnumType() {
return enumType;
}
@Override
public boolean isNullable() {
return nullable == Nullable.TRUE;
}
@Override
public M empty() {
return empty;
}
@Override
public M full() {
return create(Predicates.alwaysTrue());
}
@Override
public M create() {
return empty;
}
@Override
public M create(V value) {
return creator.create(this, CollectionsUtil.toUnmodifiableSet(value));
}
@SuppressWarnings("unchecked")
@Override
public M create(V... values) {
return creator.create(this, CollectionsUtil.toUnmodifiableSet(values));
}
@Override
public M create(Iterable values) {
return creator.create(this, Iterables.toUnmodifiableSet(values));
}
@Override
public M create(Predicate predicate) {
final Set values;
if (nullable.isTrue()) {
values = Iterables.toUnmodifiableSet(Iterables.join(enumType.getValues(), (V) null), predicate);
} else {
values = Iterables.toUnmodifiableSet(enumType.getValues(), predicate);
}
return creator.create(this, values);
}
@Override
public M create(boolean enabled) {
return create(enabled ? Predicates.alwaysTrue() : Predicates.alwaysFalse());
}
}
}