OpenConcerto

Dépôt officiel du code source de l'ERP OpenConcerto
sonarqube

svn://code.openconcerto.org/openconcerto

Rev

Rev 177 | Blame | Compare with Previous | Last modification | View Log | RSS feed

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 2011-2019 OpenConcerto, by ILM Informatique. All rights reserved.
 * 
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
 * language governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each file.
 */
 
 package org.openconcerto.utils.cc;

import org.openconcerto.utils.CollectionUtils;
import org.openconcerto.utils.CompareUtils;
import org.openconcerto.utils.Tuple2;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import net.jcip.annotations.Immutable;

/**
 * A class to specify which objects to include or exclude.
 *
 * @author Sylvain
 * @param <T> type of items
 * @see #isIncluded(Object)
 */
@Immutable
public class IncludeExclude<T> {

    static private final IncludeExclude<Object> EMPTY = new IncludeExclude<Object>(Collections.<Object> emptySet(), Collections.<Object> emptySet(), true);
    static private final IncludeExclude<Object> FULL = new IncludeExclude<Object>(null, Collections.<Object> emptySet(), true);

    @SuppressWarnings("unchecked")
    public static <T> IncludeExclude<T> getEmpty() {
        return (IncludeExclude<T>) EMPTY;
    }

    @SuppressWarnings("unchecked")
    public static <T> IncludeExclude<T> getFull() {
        return (IncludeExclude<T>) FULL;
    }

    @SafeVarargs
    public static <T> IncludeExclude<T> getNormalized(final T... includes) {
        return getNormalizedInclude(Arrays.asList(includes));
    }

    @Deprecated
    public static <T> IncludeExclude<T> getNormalized(final Collection<? extends T> includes) {
        return getNormalizedInclude(includes);
    }

    /**
     * Return the normalized version including only the passed values. E.g. if <code>includes</code>
     * is empty or <code>null</code>, this won't allocate any memory and {@link #getEmpty()} or
     * {@link #getFull()} will be returned.
     * 
     * @param <T> type of items
     * @param includes which values to include.
     * @return the normalized version.
     */
    public static <T> IncludeExclude<T> getNormalizedInclude(final Collection<? extends T> includes) {
        return getNormalized(includes, Collections.<T> emptySet());
    }

    /**
     * Return the normalized version excluding only the passed values. E.g. if <code>excludes</code>
     * is empty or <code>null</code>, this won't allocate any memory and {@link #getFull()} or
     * {@link #getEmpty()} will be returned.
     * 
     * @param <T> type of items
     * @param excludes which values to exclude.
     * @return the normalized version.
     */
    public static <T> IncludeExclude<T> getNormalizedExclude(final Collection<? extends T> excludes) {
        return getNormalized(null, excludes);
    }

    public static <T> IncludeExclude<T> getNormalized(final Collection<? extends T> includes, final Collection<? extends T> excludes) {
        return new IncludeExclude<T>(includes, excludes).normal;
    }

    private final Set<T> includes;
    private final Set<T> excludes;
    private final IncludeExclude<T> normal;

    public IncludeExclude(final Collection<? extends T> includes) {
        this(includes, Collections.<T> emptySet());
    }

    /**
     * Create a new instance.
     *
     * @param includes which objects to include, <code>null</code> meaning all.
     * @param excludes which objects to exclude, <code>null</code> meaning all.
     * @see #isIncluded(Object)
     */
    public IncludeExclude(final Collection<? extends T> includes, final Collection<? extends T> excludes) {
        this(includes, excludes, false);
    }

    private IncludeExclude(final Collection<? extends T> includes, final Collection<? extends T> excludes, final boolean isNormal) {
        this(includes == null ? null : Collections.unmodifiableSet(new HashSet<T>(includes)), excludes == null ? null : Collections.unmodifiableSet(new HashSet<T>(excludes)), false, isNormal);
    }

    private IncludeExclude(final Set<T> includes, final Set<T> excludes, final boolean areMutable, final boolean isNormal) {
        super();
        // parameter just to overload constructor
        if (areMutable)
            throw new IllegalStateException();
        this.includes = includes;
        this.excludes = excludes;
        this.normal = isNormal ? this : this.normalize();
        assert this.normal.excludes.isEmpty() || this.normal.includes == null;
    }

    private final IncludeExclude<T> normalize() {
        if (this.excludes == null || (this.includes != null && this.includes.isEmpty()))
            return getEmpty();
        if (this.excludes.isEmpty() && this.includes == null)
            return getFull();
        if (this.excludes.isEmpty() || this.includes == null)
            return this;
        return new IncludeExclude<T>(CollectionUtils.substract(this.includes, this.excludes), Collections.<T> emptySet(), true);
    }

    public final IncludeExclude<T> getNormal() {
        return this.normal;
    }

    /**
     * Whether the passed object is included or excluded by this.
     *
     * @param s an object to test.
     * @return <code>true</code> if is in include and not in exclude.
     */
    public final boolean isIncluded(final T s) {
        // "if" order is irrelevant since those methods use getNormal() and it can't be both "all"
        // and "none".
        if (this.isAllIncluded())
            return true;
        else if (this.isNoneIncluded())
            return false;
        else
            return (this.normal.includes == null || this.normal.includes.contains(s)) && !this.normal.excludes.contains(s);
    }

    public final <S extends T> Collection<S> getExcluded(final Collection<S> items) {
        if (this.areNoneIncluded(items))
            return items;
        else if (this.areAllIncluded(items))
            return Collections.emptySet();

        final Set<S> res = new HashSet<>(items);
        // avoid checks of getIncluded()
        res.removeAll(this.createIncluded(items));
        return res;
    }

    public final <S extends T> Collection<S> getIncluded(final Collection<S> items) {
        if (this.areAllIncluded(items))
            return items;
        else if (this.isNoneIncluded())
            return Collections.emptySet();
        else
            return createIncluded(items);
    }

    private <S extends T> Collection<S> createIncluded(final Collection<S> items) {
        final Set<S> res = new HashSet<>(items);
        if (this.normal.includes != null)
            res.retainAll(this.normal.includes);
        res.removeAll(this.normal.excludes);
        return res;
    }

    public final boolean areAllIncluded(final Collection<? extends T> items) {
        if (this.isAllIncluded() || items.isEmpty())
            return true;
        else if (this.isNoneIncluded())
            return false;
        else
            return (this.normal.includes == null || this.normal.includes.containsAll(items)) && Collections.disjoint(this.normal.excludes, items);
    }

    public final boolean areNoneIncluded(final Collection<? extends T> items) {
        if (this.isNoneIncluded() || items.isEmpty())
            return true;
        else if (this.isAllIncluded())
            return false;
        else
            return (this.normal.includes != null && Collections.disjoint(this.normal.includes, items)) || this.normal.excludes.containsAll(items);
    }

    /**
     * Whether this includes all objects.
     *
     * @return <code>true</code> if {@link #isIncluded(Object)} always return <code>true</code>
     */
    public final boolean isAllIncluded() {
        // use normalized value to avoid ambiguous "include all" and "exclude all"
        return this.normal == FULL;
    }

    /**
     * Whether this includes no objects.
     *
     * @return <code>true</code> if {@link #isIncluded(Object)} always return <code>false</code>
     */
    public final boolean isNoneIncluded() {
        // use normalized value to avoid ambiguous "include all" and "exclude all"
        return this.normal == EMPTY;
    }

    /**
     * The one and only object included by this.
     *
     * @return <code>true</code> if {@link #isIncluded(Object)} returns <code>true</code> for one
     *         and only one object.
     */
    public final Tuple2<Boolean, T> getSole() {
        if (this.normal.includes == null) {
            return Tuple2.create(false, null);
        } else {
            assert this.normal.excludes.isEmpty();
            final boolean res = this.normal.includes.size() == 1;
            return Tuple2.create(res, res ? this.normal.includes.iterator().next() : null);
        }
    }

    /**
     * Return the one and only object included by this, or the passed object.
     *
     * @param ifNoSole the object to return if this doesn't include exactly one object.
     * @return {@link #getSole()} or <code>ifNoSole</code>.
     */
    public final T getSole(final T ifNoSole) {
        final Tuple2<Boolean, T> sole = this.getSole();
        if (sole.get0())
            return sole.get1();
        else
            return ifNoSole;
    }

    @SafeVarargs
    public final IncludeExclude<T> include(final T... items) {
        return this.include(Arrays.asList(items));
    }

    public final IncludeExclude<T> include(final Collection<? extends T> items) {
        if (this.areAllIncluded(items))
            return this;
        else if (this.excludes == null)
            throw new IllegalStateException("Cannot include an item when excluding all");

        Set<T> newIncludes;
        if (this.includes == null || this.includes.containsAll(items)) {
            newIncludes = this.includes;
        } else {
            newIncludes = new HashSet<>(this.includes);
            newIncludes.addAll(items);
            newIncludes = Collections.unmodifiableSet(newIncludes);
        }
        Set<T> newExcludes;
        if (Collections.disjoint(this.excludes, items)) {
            newExcludes = this.excludes;
        } else {
            newExcludes = new HashSet<>(this.excludes);
            newExcludes.removeAll(items);
            newExcludes = Collections.unmodifiableSet(newExcludes);
        }

        return new IncludeExclude<T>(newIncludes, newExcludes, false, false);
    }

    @SafeVarargs
    public final IncludeExclude<T> exclude(final T... items) {
        return this.exclude(Arrays.asList(items));
    }

    public final IncludeExclude<T> exclude(final Collection<? extends T> items) {
        if (this.areNoneIncluded(items))
            return this;

        assert this.excludes != null : "!areNoneIncluded() but this.excludes == null";
        Set<T> newExcludes = new HashSet<>(this.excludes);
        newExcludes.addAll(items);
        newExcludes = Collections.unmodifiableSet(newExcludes);
        return new IncludeExclude<T>(this.includes, Collections.unmodifiableSet(newExcludes), false, false);
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((this.excludes == null) ? 0 : this.excludes.hashCode());
        result = prime * result + ((this.includes == null) ? 0 : this.includes.hashCode());
        return result;
    }

    @Override
    public boolean equals(final Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        final IncludeExclude<?> other = (IncludeExclude<?>) obj;
        return CompareUtils.equals(this.includes, other.includes) && CompareUtils.equals(this.excludes, other.excludes);
    }

    @Override
    public String toString() {
        final String suffix;
        if (this == FULL)
            suffix = " ALL";
        else if (this == EMPTY)
            suffix = " NONE";
        else
            suffix = " includes " + this.includes + " except " + this.excludes;
        return this.getClass().getSimpleName() + suffix;
    }
}