/*
 * $Id$
 *
 * Copyright 1996-2008 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code 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
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */
package com.sun.javatest;

import java.util.Arrays;
import java.util.HashSet;
import java.util.List;

import com.sun.javatest.util.I18NResourceBundle;

/**
 * A filter which uses one or more filters to implement its accept method.
 * This implementation only support immutable composites.
 * The observer messages it sends out are adjusted so that the internal filter
 * that rejected the test is returned, rather than the composite filter
 * itself.  This allows observing clients to see which non-composite filter
 * actually rejected the test in question.  Composite filters within composite
 * filters should also be transparent to the observer mechanism.
 */
public class CompositeFilter extends TestFilter {
    /**
     * This is the preferred constructor which creates a filter composed of the
     * given filters, with descriptive string from a resource bundle.
     * Inside the given resource bundle, the following keys should provided with
     * the supplied prefix:
     * <ul>
     * <li>description
     * <li>name
     * <li>reason
     * </ul>
     * See the <tt>TestFilter</tt> class for a description of these values.
     * <p>
     * An empty set of filters is accepted for convienience, but will result
     * allowing all tests passing through for filtering to be accepted.
     *
     * @param filters The filters that constitute this composite filter.
     *        Must never be null.
     * @param bundle The bundle to get the description, name and reason from.
     *        Must never be null.
     * @param prefix Prefix with which begins the I18N key for the description,
     *        name and reason.  Must never be null.
     * @see com.sun.javatest.TestFilter
     */
    public CompositeFilter(TestFilter[] filters, I18NResourceBundle bundle,
                           String prefix) {
        // XXX could start checking for null parameter
        this.filters = filters;

        description = bundle.getString(prefix + ".description");
        name = bundle.getString(prefix + ".name");
        reason = bundle.getString(prefix + ".reason");

        // paranoid checks
        if (description == null)
            description = i18n.getString("compFilter.unset.description");
        if (name == null)
            name = i18n.getString("compFilter.unset.name");
        if (reason == null)
            reason = i18n.getString("compFilter.unset.reason");
    }

    /**
     * Construct an unnamed filter composed of the given filters.
     * By using an unnamed filter, any clients querying this object for name,
     * description or rejection reason will get a general-purpose string.
     * This is usually acceptable for composites which are operating at a level
     * where messages will never be user-visible.
     * <p>
     * An empty set of filters is accepted for convienience, but will result
     * allowing all tests passing through for filtering to be accepted.
     *
     * @param filters The filters that constitute this composite filter.
     *        Must never be null.
     * @see #CompositeFilter(TestFilter[],I18NResourceBundle,String)
     */
    public CompositeFilter(TestFilter[] filters) {
        if (filters == null)
            throw new NullPointerException();
        this.filters = filters;
    }

    // ------- TestFilter ---------
    public String getName() {
        if (name == null)
            return i18n.getString("compFilter.name");
        else
            return name;
    }

    public String getDescription() {
        if (description == null)
            return i18n.getString("compFilter.description");
        else
            return description;
    }

    public String getReason() {
        if (reason == null)
            return i18n.getString("compFilter.reason");
        else
            return reason;
    }

    public boolean accepts(TestDescription td) throws Fault {
        if (filters == null || filters.length == 0)
            return true;

        try {
            for (int i = 0; i < filters.length; i++)
                if (!filters[i].accepts(td))
                    return false;

            // made it; accepted
            return true;
        }
        catch (RuntimeException e) {
            throw new Fault(i18n, "compFilter.exception", e);
        }
    }

    public boolean accepts(TestDescription td, TestFilter.Observer o)
                throws TestFilter.Fault {
        if (filters == null || filters.length == 0)
            return true;

        for (int i = 0; i < filters.length; i++)
            // this works well since the observer only has a rejected method
            if (!filters[i].accepts(td, o)) {
                return false;
            }

        return true;
    }

    /**
     * This implementation will compare the filters in the parameter filter
     * to the ones in this instance.  If the parameter is not a CompositeFilter,
     * this test fails immediately.  Recursive checks do not occur - meaning that
     * composites inside composites are not checked.  An empty set is equivalent to
     * an empty set.  This is a element-wise compare only.
     *
     * @param tf The object to be checked for equality with this one
     * @return true if this object equals the argument object
     * @see java.lang.Object#equals(Object)
     */
    public boolean equals(Object tf) {
        if (tf == null)
            return false;

        // if this composite contains one filter, should we
        // compare it to the given filter?
        // how transitive should this comparison be?...recursive composites
        if (!(tf instanceof CompositeFilter))
            return false;

        TestFilter[] thoseFilters = ((CompositeFilter)tf).getFilters();

        // no filters in both is equal
        if ((filters == null || filters.length == 0) && thoseFilters.length == 0)
            return true;

        // both sets have at least 1 element
        // if they don't have the same size, then they are not equal
        if (filters.length != thoseFilters.length)
            return false;

        return equals(filters, thoseFilters);
    }

    // ---------- new methods ----------
    /**
     * Return the filters being used internally by this composite.
     *
     * @return The filters in use.  Zero length array if none.  Never null.
     *         Shallow copy.
     */
    public TestFilter[] getFilters() {
        if (filters == null)
            return new TestFilter[0];

        TestFilter[] copy = new TestFilter[filters.length];
        System.arraycopy(filters, 0, copy, 0, filters.length);

        return copy;
    }

    /**
     * Check if two arrays are equal, using set-equality.
     *
     * @param array1 First set of filters; may not be null.
     * @param array2 Second set of filters; may not be null.
     * @return true of the two arrays of filters are equivalent.
     */
    public static boolean equals(TestFilter[] array1, TestFilter[] array2) {
        List list1 = Arrays.asList(array1);
        List list2 = Arrays.asList(array2);

        HashSet set1 = new HashSet(list1);
        HashSet set2 = new HashSet(list2);

        return set1.equals(set2);
    }

    private TestFilter[] filters;
    private String description;
    private String name;
    private String reason;
    private static I18NResourceBundle i18n = I18NResourceBundle.getBundleForClass(CompositeFilter.class);
}
