/*
 * 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.clerezza.rdf.core.access.security;

import java.security.AccessControlException;
import java.security.AccessController;
import java.security.AllPermission;
import java.security.Permission;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.concurrent.locks.Lock;
import org.apache.clerezza.rdf.core.BNode;
import org.apache.clerezza.rdf.core.LiteralFactory;
import org.apache.clerezza.rdf.core.NonLiteral;
import org.apache.clerezza.rdf.core.Resource;
import org.apache.clerezza.rdf.core.Triple;
import org.apache.clerezza.rdf.core.TypedLiteral;
import org.apache.clerezza.rdf.core.UriRef;
import org.apache.clerezza.rdf.core.access.LockableMGraph;
import org.apache.clerezza.rdf.core.access.NoSuchEntityException;
import org.apache.clerezza.rdf.core.access.TcManager;
import org.apache.clerezza.rdf.core.impl.TripleImpl;
import org.apache.clerezza.utils.security.PermissionParser;

/**
 * Controls the permissions needed to access a triple collection provided by
 * <code>TcManager</code>.
 *
 * Clients with a ConfigureTcAcessPermission can set the permissions required to
 * access a TripleCollection. These permissions are stored persistently in an
 * MGraph named urn:x-localinstance:/graph-access.graph
 *
 * Clients should get an instance from TcManager.getTcAccessController()
 * 
 * @author reto
 */
public abstract class TcAccessController {

    private final TcManager tcManager;
    private final UriRef permissionGraphName = new UriRef("urn:x-localinstance:/graph-access.graph");
    //we can't rely on ontology plugin in rdf core
    private String ontologyNamespace = "http://clerezza.apache.org/2010/07/10/graphpermssions#";
    private final UriRef readPermissionListProperty = new UriRef(ontologyNamespace + "readPermissionList");
    private final UriRef readWritePermissionListProperty = new UriRef(ontologyNamespace + "readWritePermissionList");
    /**
     * The first item in the subject RDF list.
     */
    public static final UriRef first = new UriRef("http://www.w3.org/1999/02/22-rdf-syntax-ns#first");
    /**
     * The rest of the subject RDF list after the first item.
     */
    public static final UriRef rest = new UriRef("http://www.w3.org/1999/02/22-rdf-syntax-ns#rest");
    public static final UriRef rdfNil = new UriRef("http://www.w3.org/1999/02/22-rdf-syntax-ns#nil");
    private final Map<UriRef, Collection<Permission>> readPermissionCache =
            Collections.synchronizedMap(new HashMap<UriRef, Collection<Permission>>());
    private final Map<UriRef, Collection<Permission>> readWritePermissionCache =
            Collections.synchronizedMap(new HashMap<UriRef, Collection<Permission>>());

    /**
     *
     * @param tcManager the tcManager used to locate urn:x-localinstance:/graph-access.graph
     */
    public TcAccessController() {
        this.tcManager = getTcManager();
    }

    public void checkReadPermission(UriRef tripleCollectionUri) {
        if (tripleCollectionUri.equals(permissionGraphName)) {
            //This is world readable, as this prevents as from doingf things as
            //priviledged during verfification
            return;
        }
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            //will AllPermissions the rest is obsolete
            try {
                AccessController.checkPermission(new AllPermission());
            } catch (AccessControlException e) {
                Collection<Permission> perms = getRequiredReadPermissions(tripleCollectionUri);
                if (perms.size() > 0) {
                    for (Permission permission : perms) {
                        AccessController.checkPermission(permission);
                    }
                } else {
                    AccessController.checkPermission(new TcPermission(
                            tripleCollectionUri.getUnicodeString(), TcPermission.READ));
                }
            }
        }
    }

    public void checkReadWritePermission(UriRef tripleCollectionUri) {
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            //will AllPermissions the rest is obsolete
            try {
                AccessController.checkPermission(new AllPermission());
            } catch (AccessControlException e) {
                if (tripleCollectionUri.equals(permissionGraphName)) {
                    AccessController.checkPermission(new TcPermission(
                            tripleCollectionUri.getUnicodeString(), TcPermission.READWRITE));
                } else {
                    Collection<Permission> perms = getRequiredReadWritePermissions(tripleCollectionUri);
                    if (perms.size() > 0) {
                        for (Permission permission : perms) {
                            AccessController.checkPermission(permission);
                        }
                    } else {
                        AccessController.checkPermission(new TcPermission(
                                tripleCollectionUri.getUnicodeString(), TcPermission.READWRITE));
                    }
                }
            }
        }
    }

    /**
     * Set the set of permissions required for read access to a triple-collection, if
     * the set is non-empty the default TCPermisson is no longer required.
     *
     * @param tripleCollectionUri
     * @param permissionDescriptions
     */
    public void setRequiredReadPermissionStrings(UriRef tripleCollectionUri,
            Collection<String> permissionDescriptions) {
        readPermissionCache.remove(tripleCollectionUri);
        final LockableMGraph permissionMGraph = getOrCreatePermisionGraph();
        Lock l = permissionMGraph.getLock().writeLock();
        l.lock();
        try {
            removeExistingRequiredReadPermissions(tripleCollectionUri, permissionMGraph);
            final NonLiteral permissionList = createList(permissionDescriptions.iterator(), permissionMGraph);
            permissionMGraph.add(new TripleImpl(tripleCollectionUri,
                    readPermissionListProperty, permissionList));
        } finally {
            l.unlock();
        }
    }

    /**
     * Set the set of permissions required for read access to a triple-collection, if
     * the set is non-empty the default TCPermisson is no longer required.
     *
     * @param tripleCollectionUri
     * @param permissionDescriptions
     */
    public void setRequiredReadPermissions(UriRef tripleCollectionUri,
            Collection<Permission> permissions) {
        Collection<String> permissionStrings = new ArrayList<String>();
        for (Permission permission : permissions) {
            permissionStrings.add(permission.toString());
        }
        setRequiredReadPermissionStrings(tripleCollectionUri, permissionStrings);
    }

    /**
     * Set the set of permissions required for read-write access to a
     * triple-collection, if
     * the set is non-empty the default TCPermisson is no longer required.
     *
     * @param tripleCollectionUri
     * @param permissionDescriptions
     */
    public void setRequiredReadWritePermissionStrings(UriRef tripleCollectionUri,
            Collection<String> permissionDescriptions) {
        readWritePermissionCache.remove(tripleCollectionUri);
        final LockableMGraph permissionMGraph = getOrCreatePermisionGraph();
        Lock l = permissionMGraph.getLock().writeLock();
        l.lock();
        try {
            removeExistingRequiredReadPermissions(tripleCollectionUri, permissionMGraph);
            final NonLiteral permissionList = createList(permissionDescriptions.iterator(), permissionMGraph);
            permissionMGraph.add(new TripleImpl(tripleCollectionUri,
                    readWritePermissionListProperty, permissionList));
        } finally {
            l.unlock();
        }
    }

    /**
     * Set the set of permissions required for read-write access to a
     * triple-collection, if
     * the set is non-empty the default TCPermisson is no longer required.
     *
     * @param tripleCollectionUri
     * @param permissionDescriptions
     */
    public void setRequiredReadWritePermissions(UriRef tripleCollectionUri,
            Collection<Permission> permissions) {
        Collection<String> permissionStrings = new ArrayList<String>();
        for (Permission permission : permissions) {
            permissionStrings.add(permission.toString());
        }
        setRequiredReadWritePermissionStrings(tripleCollectionUri, permissionStrings);
    }

    /**
     * Get the set of permissions required for read access to the
     * triple-collection, the set may be empty meaning that the default
     * TCPermission is required.
     *
     * @param tripleCollectionUri
     * @return the collection of permissions
     */
    public Collection<Permission> getRequiredReadPermissions(UriRef tripleCollectionUri) {
        Collection<Permission> result = readPermissionCache.get(tripleCollectionUri);
        if (result == null) {
            result = new ArrayList<Permission>();
            Collection<String> permissionStrings = getRequiredReadPermissionStrings(tripleCollectionUri);
            for (String string : permissionStrings) {
                result.add(PermissionParser.getPermission(string, getClass().getClassLoader()));
            }
            readPermissionCache.put(tripleCollectionUri, result);
        }
        return result;
    }

    /**
     * Get the set of permissions required for read-write access to the
     * triple-collection, the set may be empty meaning that the default
     * TCPermission is required.
     *
     * @param tripleCollectionUri
     * @return the collection of permissions
     */
    public Collection<Permission> getRequiredReadWritePermissions(UriRef tripleCollectionUri) {
        Collection<Permission> result = readWritePermissionCache.get(tripleCollectionUri);
        if (result == null) {
            result = new ArrayList<Permission>();
            Collection<String> permissionStrings = getRequiredReadWritePermissionStrings(tripleCollectionUri);
            for (String string : permissionStrings) {
                result.add(PermissionParser.getPermission(string, getClass().getClassLoader()));
            }
            readWritePermissionCache.put(tripleCollectionUri, result);
        }
        return result;
    }

    private NonLiteral createList(Iterator<String> iterator, LockableMGraph permissionMGraph) {
        if (!iterator.hasNext()) {
            return rdfNil;
        }
        final BNode result = new BNode();
        permissionMGraph.add(new TripleImpl(result, first,
                LiteralFactory.getInstance().createTypedLiteral(iterator.next())));
        permissionMGraph.add(new TripleImpl(result, rest,
                createList(iterator, permissionMGraph)));
        return result;

    }

    //called withiong write-lock
    private void removeExistingRequiredReadPermissions(UriRef tripleCollectionUri,
            LockableMGraph permissionMGraph) {
        try {
            Triple t = permissionMGraph.filter(tripleCollectionUri, readPermissionListProperty, null).next();
            Resource list = t.getObject();
            removeList((NonLiteral) list, permissionMGraph);
            permissionMGraph.remove(t);
        } catch (NoSuchElementException e) {
            //There was no existing to remove
        }
    }

    private void removeList(NonLiteral list, LockableMGraph permissionMGraph) {
        try {
            Triple t = permissionMGraph.filter(list, rest, null).next();
            Resource restList = t.getObject();
            removeList((NonLiteral) restList, permissionMGraph);
            permissionMGraph.remove(t);
            Iterator<Triple> iter = permissionMGraph.filter(list, first, null);
            iter.next();
            iter.remove();
        } catch (NoSuchElementException e) {
            //if it has no rest its rdf:NIL and has no first
        }
    }

    private Collection<String> getRequiredReadWritePermissionStrings(final UriRef tripleCollectionUri) {
        return getRequiredPermissionStrings(tripleCollectionUri, readWritePermissionListProperty);
    }
    private Collection<String> getRequiredReadPermissionStrings(final UriRef tripleCollectionUri) {
        return getRequiredPermissionStrings(tripleCollectionUri, readPermissionListProperty);
    }
    private Collection<String> getRequiredPermissionStrings(final UriRef tripleCollectionUri, UriRef property) {
        try {
            final LockableMGraph permissionMGraph = tcManager.getMGraph(permissionGraphName);
            Lock l = permissionMGraph.getLock().readLock();
            l.lock();
            try {
                Triple t = permissionMGraph.filter(tripleCollectionUri, property, null).next();
                NonLiteral list = (NonLiteral) t.getObject();
                LinkedList<String> result = new LinkedList<String>();
                readList(list, permissionMGraph, result);
                return result;
            } catch (NoSuchElementException e) {
                return new ArrayList<String>(0);
            } finally {
                l.unlock();
            }
        } catch (NoSuchEntityException e) {
            return new ArrayList<String>(0);
        }
    }

    private void readList(NonLiteral list, LockableMGraph permissionMGraph, LinkedList<String> target) {
        if (list.equals(rdfNil)) {
            return;
        }
        Triple restTriple = permissionMGraph.filter(list, rest, null).next();
        NonLiteral restList = (NonLiteral) restTriple.getObject();
        readList(restList, permissionMGraph, target);
        Triple firstTriple = permissionMGraph.filter(list, first, null).next();
        TypedLiteral firstValue = (TypedLiteral) firstTriple.getObject();
        String value = LiteralFactory.getInstance().createObject(String.class, firstValue);
        target.addFirst(value);
    }

    private LockableMGraph getOrCreatePermisionGraph() {
        try {
            return tcManager.getMGraph(permissionGraphName);
        } catch (NoSuchEntityException e) {
            return tcManager.createMGraph(permissionGraphName);
        }
    }

    /**
     * Note that this will only be invoked once
     * @return 
     */
    protected abstract TcManager getTcManager();
}
