/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.jackrabbit.core.security.principal;

import java.security.Principal;
import java.security.acl.Group;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;

import javax.jcr.RepositoryException;
import javax.jcr.Session;

import org.apache.jackrabbit.api.security.principal.ItemBasedPrincipal;
import org.apache.jackrabbit.api.security.principal.JackrabbitPrincipal;
import org.apache.jackrabbit.api.security.principal.PrincipalIterator;
import org.apache.jackrabbit.api.security.principal.PrincipalManager;

/**
 * This principal manager implementation uses the {@link DefaultPrincipalProvider}
 * in order to dispatch the respective requests and assemble the required
 * data. It is bound to a session and therefore obliges the access restrictions
 * of the respective subject.
 */
public class PrincipalManagerImpl implements PrincipalManager {

    /** the Session this manager has been created for*/
    private final Session session;

    /** the principalProviders */
    private final PrincipalProvider[] providers;

    private boolean closed;

    /**
     * Creates a new default principal manager implementation.
     *
     * @param session the underlying session
     * @param providers the providers
     */
    public PrincipalManagerImpl(Session session, PrincipalProvider[] providers) {
        this.session = session;
        this.providers = providers;
        closed = false;
    }

    /**
     * {@inheritDoc}
     */
    public boolean hasPrincipal(String principalName) {
        return internalGetPrincipal(principalName) != null;
    }

    /**
     * {@inheritDoc}
     */
    public Principal getPrincipal(String principalName) {
        return internalGetPrincipal(principalName);
    }

    /**
     * {@inheritDoc}
     */
    public PrincipalIterator findPrincipals(String simpleFilter) {
        checkIsValid();
        List<CheckedIteratorEntry> entries = new ArrayList<CheckedIteratorEntry>(providers.length);
        for (PrincipalProvider pp : providers) {
            PrincipalIterator it = pp.findPrincipals(simpleFilter);
            if (it.hasNext()) {
                entries.add(new CheckedIteratorEntry(it, pp));
            }
        }
        return new CheckedPrincipalIterator(entries);
    }

    /**
     * {@inheritDoc}
     */
    public PrincipalIterator findPrincipals(String simpleFilter, int searchType) {
        checkIsValid();
        List<CheckedIteratorEntry> entries = new ArrayList<CheckedIteratorEntry>(providers.length);
        for (PrincipalProvider pp : providers) {
            PrincipalIterator it = pp.findPrincipals(simpleFilter, searchType);
            if (it.hasNext()) {
                entries.add(new CheckedIteratorEntry(it, pp));
            }
        }
        return new CheckedPrincipalIterator(entries);
    }

    /**
     * {@inheritDoc}
     * @param searchType
     */
    public PrincipalIterator getPrincipals(int searchType) {
        checkIsValid();
        List<CheckedIteratorEntry> entries = new ArrayList<CheckedIteratorEntry>(providers.length);
        for (PrincipalProvider pp : providers) {
            PrincipalIterator it = pp.getPrincipals(searchType);
            if (it.hasNext()) {
                entries.add(new CheckedIteratorEntry(it, pp));
            }
        }
        return new CheckedPrincipalIterator(entries);
    }

    /**
     * {@inheritDoc}
     */
    public PrincipalIterator getGroupMembership(Principal principal) {
        checkIsValid();
        List<CheckedIteratorEntry> entries =  new ArrayList<CheckedIteratorEntry>(providers.length + 1);
        for (PrincipalProvider pp : providers) {
            PrincipalIterator groups = pp.getGroupMembership(principal);
            if (groups.hasNext()) {
                entries.add(new CheckedIteratorEntry(groups, pp));
            }
        }
        // additional entry for the 'everyone' group
        if (!(principal instanceof EveryonePrincipal)) {
            Iterator<Principal> it = Collections.singletonList(getEveryone()).iterator();
            entries.add(new CheckedIteratorEntry(it, null));
        }
        return new CheckedPrincipalIterator(entries);
    }

    /**
     * {@inheritDoc}
     */
    public Principal getEveryone() {
        checkIsValid();
        Principal everyone = getPrincipal(EveryonePrincipal.NAME);
        if (everyone == null) {
            everyone = EveryonePrincipal.getInstance();
        }
        return everyone;
    }

    //--------------------------------------------------------------------------
    /**
     * Check if the instance has been closed.
     *
     * @throws IllegalStateException if this instance was closed.
     */
    private void checkIsValid() {
        if (closed) {
            throw new IllegalStateException("PrincipalManagerImpl instance has been closed.");
        }
    }

    /**
     * @param principalName the name of the principal
     * @return The principal with the given name or <code>null</code> if none
     * of the providers knows that principal of if the Session is not allowed
     * to see it.
     */
    private Principal internalGetPrincipal(String principalName) {
        checkIsValid();
        for (PrincipalProvider provider : providers) {
            Principal principal = provider.getPrincipal(principalName);
            if (principal != null && provider.canReadPrincipal(session, principal)) {
                return disguise(principal, provider);
            }
        }
        // nothing found or not allowed to see it.
        return null;
    }

    /**
     * @param principal the principal
     * @param provider the provider
     * @return A group that only reveals those members that are visible to the
     * current session or the specified principal if its not a group or the
     * everyone principal.
     */
    private Principal disguise(Principal principal, PrincipalProvider provider) {
        if (!(principal instanceof Group) || principal instanceof EveryonePrincipal) {
            // nothing to do.
            return principal;
        }
        Group gr = (Group) principal;
        // make sure all groups except for the 'everyone' group expose only
        // principals visible to the session.
        if (principal instanceof ItemBasedPrincipal) {
            return new ItemBasedCheckedGroup(gr, provider);
        } else {
            return new CheckedGroup(gr, provider);
        }
    }

    //--------------------------------------------------------------------------
    /**
     * An implementation of the <code>Group</code> interface that wraps another
     * Group and makes sure, that all exposed members are visible to the Session
     * the <code>PrincipalManager</code> has been built for. This is required
     * due to the fact, that the principal provider is not bound to a particular
     * Session object.
     */
    private class CheckedGroup implements Group, JackrabbitPrincipal {

        final Group delegatee;
        private final PrincipalProvider provider;

        private CheckedGroup(Group delegatee, PrincipalProvider provider) {
            this.delegatee = delegatee;
            this.provider = provider;
        }

        public boolean addMember(Principal user) {
            throw new UnsupportedOperationException("Not implemented");
        }

        public boolean removeMember(Principal user) {
            throw new UnsupportedOperationException("Not implemented");
        }

        public boolean isMember(Principal member) {
            return delegatee.isMember(member);
        }

        public Enumeration<? extends Principal> members() {
            Iterator<? extends Principal> it = Collections.list(delegatee.members()).iterator();
            final PrincipalIterator members = new CheckedPrincipalIterator(it, provider);
            return new Enumeration<Principal>() {
                public boolean hasMoreElements() {
                    return members.hasNext();
                }
                public Principal nextElement() {
                    return members.nextPrincipal();
                }
            };
        }

        public String getName() {
            return delegatee.getName();
        }

        //---------------------------------------------------------< Object >---
        @Override
        public int hashCode() {
            return delegatee.hashCode();
        }

        @Override
        public boolean equals(Object obj) {
            return delegatee.equals(obj instanceof CheckedGroup ? ((CheckedGroup) obj).delegatee : obj);
        }
    }

    /**
     * Same as {@link CheckedGroup} but wrapping an ItemBasePrincipal.
     */
    private class ItemBasedCheckedGroup extends CheckedGroup implements ItemBasedPrincipal {

        private ItemBasedCheckedGroup(Group delegatee, PrincipalProvider provider) {
            super(delegatee, provider);
            if (!(delegatee instanceof ItemBasedPrincipal)) {
                throw new IllegalArgumentException();
            }
        }

        public String getPath() throws RepositoryException {
            return ((ItemBasedPrincipal) delegatee).getPath();
        }
    }

    //--------------------------------------------------------------------------
    /**
     * A PrincipalIterator implementation that tests for each principal
     * in the passed base iterators whether it is visible to the Session
     * the PrincipalManager has been built for. A principal that is not
     * accessible is skipped during the iteration.
     */
    private class CheckedPrincipalIterator extends AbstractPrincipalIterator {

        private final List<CheckedIteratorEntry> entries;

        private CheckedPrincipalIterator(Iterator<? extends Principal> it, PrincipalProvider provider) {
            entries = new ArrayList<CheckedIteratorEntry>(1);
            entries.add(new CheckedIteratorEntry(it, provider));
            next = seekNext();
        }

        private CheckedPrincipalIterator(List<CheckedIteratorEntry> entries) {
            this.entries = new ArrayList<CheckedIteratorEntry>(entries);
            next = seekNext();
        }

        /**
         * @see org.apache.jackrabbit.core.security.principal.AbstractPrincipalIterator#seekNext()
         */
        @Override
        protected final Principal seekNext() {
            while (!entries.isEmpty()) {
                // first test if current iterator has more elements
                CheckedIteratorEntry current = entries.get(0);
                Iterator<? extends Principal> iterator = current.iterator;
                while (iterator.hasNext()) {
                    Principal chk = iterator.next();
                    if (current.provider == null ||
                        current.provider.canReadPrincipal(session, chk)) {
                        return disguise(chk, current.provider);
                    }
                }
                // no more elements in current iterator -> move to next iterator.
                entries.remove(0);
            }
            return null;
        }
    }

    //--------------------------------------------------------------------------
    /**
     *
     */
    private static class CheckedIteratorEntry {

        private final PrincipalProvider provider;
        private final Iterator<? extends Principal> iterator;

        private CheckedIteratorEntry(Iterator<? extends Principal> iterator, PrincipalProvider provider) {
            this.iterator = iterator;
            this.provider = provider;
        }
    }
}
