/*
* Copyright (c) 2015 NOVA, All rights reserved.
* This library is free software, licensed under GNU Lesser General Public License version 3
*
* This file is part of NOVA.
*
* NOVA is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NOVA is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NOVA. If not, see <http://www.gnu.org/licenses/>.
*/
package nova.core.util;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.Objects;
import java.util.Set;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
/**
* This class is used to mark certain values from specified enum as allowed.
* Note that you must specify default state via #allowAll or #blockAll methods.
*
* Allows iteration of all allowed elements.
*
* @param <T> The enum
*/
public class EnumSelector<T extends Enum<T>> implements Iterable<T> {
private EnumSet<T> exceptions;
private boolean defaultAllow, defaultBlock = false;
private boolean locked = false;
private EnumSelector(Class<T> enumClass) {
exceptions = EnumSet.noneOf(enumClass);
}
/**
* Creates a new instance of EnumSelector for the given type.
*
* @return an instance of EnumSelector for the given type.
*/
public static <T extends Enum<T>> EnumSelector<T> of(Class<T> enumClass) {
return new EnumSelector<>(enumClass);
}
private void checkWritable() {
if (locked)
throw new IllegalStateException("No edits are allowed after EnumSelector has been locked.");
}
private void checkReadable() {
if (!locked)
throw new IllegalStateException("Cannot use EnumSelector that is not locked.");
}
/**
* Make the EnumSelector allow all for the given type by default.
* <p>
* Use {@link #apart(java.lang.Enum)} to specify what should be blocked.
*
* @see #blockAll()
* @see #apart(java.lang.Enum)
* @return this
* @throws IllegalStateException If the EnumSelector has been {@link #lock() locked}.
*/
public EnumSelector<T> allowAll() {
checkWritable();
if (!defaultBlock)
defaultAllow = true;
else
throw new IllegalStateException("You can't allow all enum values when you are already blocking them.");
return this;
}
/**
* Make the EnumSelector block all for the given type by default.
* <p>
* Use {@link #apart(java.lang.Enum)} to specify what should be allowed.
*
* @see #allowAll()
* @see #apart(java.lang.Enum)
* @return this
* @throws IllegalStateException If the EnumSelector has been {@link #lock() locked}.
*/
public EnumSelector<T> blockAll() {
checkWritable();
if (!defaultAllow)
defaultBlock = true;
else
throw new IllegalStateException("You can't block all enum values when you are already allowing them.");
return this;
}
/**
* Specify which {@code enum} values should have behavior opposite of the default
*
* @see #allowAll()
* @see #blockAll()
* @param value The given {@code enum} value that should have behavior opposite of the default.
* @return this
* @throws IllegalStateException If the EnumSelector has been {@link #lock() locked}.
*/
public EnumSelector<T> apart(T value) {
checkWritable();
exceptions.add(value);
return this;
}
/**
* Specify which {@code enum} values should have behavior opposite of the default
*
* @see #allowAll()
* @see #blockAll()
* @param first The first {@code enum} value that should have behavior opposite of the default.
* @param second The second {@code enum} value that should have behavior opposite of the default.
* @return this
* @throws IllegalStateException If the EnumSelector has been {@link #lock() locked}.
*/
public EnumSelector<T> apart(T first, T second) {
checkWritable();
exceptions.add(first);
exceptions.add(second);
return this;
}
/**
* Specify which {@code enum} values should have behavior opposite of the default
*
* @see #allowAll()
* @see #blockAll()
* @param values The given {@code enum} values that should have behavior opposite of the default.
* @return this
* @throws IllegalStateException If the EnumSelector has been {@link #lock() locked}.
*/
@SuppressWarnings("unchecked")
public EnumSelector<T> apart(T... values) {
checkWritable();
exceptions.addAll(Arrays.asList(values));
return this;
}
/**
* Specify which {@code enum} values should have behavior opposite of the default
*
* @see #allowAll()
* @see #blockAll()
* @param values The given {@code enum} values that should have behavior opposite of the default.
* @return this
* @throws IllegalStateException If the EnumSelector has been {@link #lock() locked},
* or if the {@code values} parameter is an EnumSelector instance which has
* not been {@link #lock() locked}.
*/
@SuppressWarnings("unchecked")
public EnumSelector<T> apart(Iterable<T> values) {
checkWritable();
if (values instanceof EnumSelector) {
EnumSelector<T> other = (EnumSelector<T>) values;
try {
other.checkReadable();
} catch (IllegalStateException e) {
throw new IllegalArgumentException(e);
}
if (!defaultAllow && !defaultBlock)
throw new IllegalStateException("Cannot call EnumSelector.apart(EnumSelector) without specifying default behaviour.");
if ((defaultAllow && other.defaultBlock) || (defaultBlock && other.defaultBlock)) {
this.exceptions.addAll(other.exceptions);
} else {
this.exceptions.addAll(EnumSet.complementOf(other.exceptions));
}
} else if (values instanceof Collection) {
exceptions.addAll((Collection<T>)values);
} else {
values.forEach(exceptions::add);
}
return this;
}
/**
* Lock the EnumSelector, making it immutable.
* Required for using of all getter methods.
*
* @see #allowAll()
* @see #blockAll()
* @see #locked()
* @return this
* @throws IllegalStateException If the EnumSelector does not have specified default behavior.
*/
public EnumSelector<T> lock() {
if (defaultAllow || defaultBlock)
locked = true;
else
throw new IllegalStateException("Cannot lock EnumSelector without specifying default behaviour.");
return this;
}
/**
* Check if the EnumSelector instance has been locked.
*
* @return The locked status.
*/
public boolean locked() {
return locked;
}
/**
* Check if the {@code enum} value is allowed by this EnumSelector.
*
* @param value The {@code enum} value to test.
* @return If the {@code enum} value is allowed by this EnumSelector.
* @throws IllegalStateException If the EnumSelector has not been {@link #lock() locked}.
*/
public boolean allows(T value) {
checkReadable();
return defaultAllow ^ exceptions.contains(value);
}
/**
* Check if the EnumSelector allows all values.
*
* @return If the EnumSelector allows all values.
* @throws IllegalStateException If the EnumSelector has not been {@link #lock() locked}.
*/
public boolean allowsAll() {
checkReadable();
return (defaultAllow && exceptions.isEmpty()) || (defaultBlock && EnumSet.complementOf(exceptions).isEmpty());
}
/**
* Check if the {@code enum} value is blocked by this EnumSelector.
*
* @param value The {@code enum} value to test.
* @return If the {@code enum} value is blocked by this EnumSelector.
* @throws IllegalStateException If the EnumSelector has not been {@link #lock() locked}.
*/
public boolean blocks(T value) {
checkReadable();
return defaultBlock ^ exceptions.contains(value);
}
/**
* Check if the EnumSelector blocks all values.
*
* @return If the EnumSelector blocks all values.
* @throws IllegalStateException If the EnumSelector has not been {@link #lock() locked}.
*/
public boolean blocksAll() {
checkReadable();
return (defaultBlock && exceptions.isEmpty()) || (defaultAllow && EnumSet.complementOf(exceptions).isEmpty());
}
/**
* Returns an iterator of all the allowed elements in this EnumSelector.
*
* @return The iterator.
* @throws IllegalStateException If the EnumSelector has not been {@link #lock() locked}.
*/
@Override
public Iterator<T> iterator() {
checkReadable();
return toSet().iterator();
}
/**
* Returns a spliterator of all the allowed elements in this EnumSelector.
*
* @return The spliterator.
* @throws IllegalStateException If the EnumSelector has not been {@link #lock() locked}.
*/
@Override
public Spliterator<T> spliterator() {
checkReadable();
Set<T> set = toSet();
return Spliterators.spliterator(set.iterator(), set.size(),
Spliterator.DISTINCT | Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.IMMUTABLE);
}
/**
* Returns a sequential stream of all the allowed elements in this EnumSelector.
*
* @return The stream.
* @throws IllegalStateException If the EnumSelector has not been {@link #lock() locked}.
*/
public Stream<T> stream() {
checkReadable();
return StreamSupport.stream(spliterator(), false);
}
/**
* Returns a parallel stream of all the allowed elements in this EnumSelector.
*
* @return The stream.
* @throws IllegalStateException If the EnumSelector has not been {@link #lock() locked}.
*/
public Stream<T> parallelStream() {
checkReadable();
return StreamSupport.stream(spliterator(), true);
}
/**
* Returns a Set instance of all the allowed elements in this EnumSelector.
*
* @return The set.
* @throws IllegalStateException If the EnumSelector has not been {@link #lock() locked}.
*/
public Set<T> toSet() {
checkReadable();
return Collections.unmodifiableSet(defaultBlock ? exceptions : EnumSet.complementOf(exceptions));
}
/**
* Returns the count of all the allowed elements in this EnumSelector.
*
* @return The count.
* @throws IllegalStateException If the EnumSelector has not been {@link #lock() locked}.
*/
public int size() {
checkReadable();
return (defaultBlock ? exceptions : EnumSet.complementOf(exceptions)).size();
}
/**
* {@inheritDoc}
*
* @see EnumSet#toString()
* @return The same result as calling {@link #toSet()}.{@link EnumSet#toString() toString()}.
* @throws IllegalStateException If the EnumSelector has not been {@link #lock() locked}.
*/
@Override
public String toString() {
return (defaultBlock ? exceptions : EnumSet.complementOf(exceptions)).toString();
}
/**
* {@inheritDoc}
*
* @see EnumSet#hashCode()
* @return The hash calculated from {@link #toSet()}.{@link EnumSet#hashCode() hashCode()}.
* @throws IllegalStateException If the EnumSelector has not been {@link #lock() locked}.
*/
@Override
public int hashCode() {
checkReadable();
int hash = 5;
hash = 79 * hash + Objects.hashCode(defaultBlock ? exceptions : EnumSet.complementOf(exceptions));
return hash;
}
/**
* {@inheritDoc}
*
* @see EnumSet#equals(Object)
* @return true if this EnumSelector allows the exact same values as the other EnumSelector
* @throws IllegalStateException If the EnumSelector has not been {@link #lock() locked}.
*/
@Override
public boolean equals(Object other) {
checkReadable();
if (this == other) return true;
if (other == null || getClass() != other.getClass()) return false;
return this.toSet().equals(((EnumSelector<?>)other).toSet());
}
}