/*
 * Copyright (c) 1996, 2006, Oracle and/or its affiliates. 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.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle 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 Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package sun.security.provider;

import java.io.*;
import java.util.*;
import java.security.*;

/**
 * An implementation of IdentityScope as a persistent identity
 * database.
 *
 * @see Identity
 * @see Key
 *
 * @author Benjamin Renaud
 */
public
class IdentityDatabase extends IdentityScope implements Serializable {

    /** use serialVersionUID from JDK 1.1. for interoperability */
    private static final long serialVersionUID = 4923799573357658384L;

    /* Are we debugging? */
    private static final boolean debug = false;

    /* Are we printing out error messages? */
    private static final boolean error = true;

    /* The source file, if any, for this database.*/
    File sourceFile;

    /* The private representation of the database.*/
    Hashtable<String, Identity> identities;

    IdentityDatabase() throws InvalidParameterException {
        this("restoring...");
    }

    /**
     * Construct a new, empty database with a specified source file.
     *
     * @param file the source file.
     */
    public IdentityDatabase(File file) throws InvalidParameterException {
        this(file.getName());
        sourceFile = file;
    }

    /**
     * Construct a new, empty database.
     */
    public IdentityDatabase(String name) throws InvalidParameterException {
        super(name);
        identities = new Hashtable<String, Identity>();
    }

    /**
     * Initialize an identity database from a stream. The stream should
     * contain data to initialized a serialized IdentityDatabase
     * object.
     *
     * @param is the input stream from which to restore the database.
     *
     * @exception IOException if a stream IO exception occurs
     */
    public static IdentityDatabase fromStream(InputStream is)
    throws IOException {
        IdentityDatabase db = null;
        try {
            ObjectInputStream ois = new ObjectInputStream(is);
            db = (IdentityDatabase)ois.readObject();
        } catch (ClassNotFoundException e) {
            // this can't happen.
            debug("This should not be happening.", e);
            error(
                "The version of the database is obsolete. Cannot initialize.");

        } catch (InvalidClassException e) {
            // this may happen in developers workspaces happen.
            debug("This should not be happening.", e);
            error("Unable to initialize system identity scope: " +
                  " InvalidClassException. \nThis is most likely due to " +
                  "a serialization versioning problem: a class used in " +
                  "key management was obsoleted");

        } catch (StreamCorruptedException e) {
            debug("The serialization stream is corrupted. Unable to load.", e);
            error("Unable to initialize system identity scope." +
                  " StreamCorruptedException.");
        }

        if (db == null) {
            db = new IdentityDatabase("uninitialized");
        }

        return db;
    }

    /**
     * Initialize an IdentityDatabase from file.
     *
     * @param f the filename where the identity database is stored.
     *
     * @exception IOException a file-related exception occurs (e.g.
     * the directory of the file passed does not exists, etc.
     *
     * @IOException if a file IO exception occurs.
     */
    public static IdentityDatabase fromFile(File f) throws IOException {
        FileInputStream fis = new FileInputStream(f);
        IdentityDatabase edb = fromStream(fis);
        edb.sourceFile = f;
        return edb;
    }



    /**
     * @return the number of identities in the database.
     */
   public int size() {
       return identities.size();
   }


    /**
     * @param name the name of the identity to be retrieved.
     *
     * @return the identity named name, or null if there are
     * no identities named name in the database.
     */
    public Identity getIdentity(String name) {
        Identity id = identities.get(name);
        if (id instanceof Signer) {
            localCheck("get.signer");
        }
        return id;
    }

    /**
     * Get an identity by key.
     *
     * @param name the key of the identity to be retrieved.
     *
     * @return the identity with a given key, or null if there are no
     * identities with that key in the database.
     */
    public Identity getIdentity(PublicKey key) {
        if (key == null) {
            return null;
        }
        Enumeration<Identity> e = identities();
        while (e.hasMoreElements()) {
            Identity i = e.nextElement();
            PublicKey k = i.getPublicKey();
            if (k != null && keyEqual(k, key)) {
                if (i instanceof Signer) {
                    localCheck("get.signer");
                }
                return i;
            }
        }
        return null;
    }

    private boolean keyEqual(Key key1, Key key2) {
        if (key1 == key2) {
            return true;
        } else {
            return MessageDigest.isEqual(key1.getEncoded(), key2.getEncoded());
        }
    }

    /**
     * Adds an identity to the database.
     *
     * @param identity the identity to be added.
     *
     * @exception KeyManagementException if a name or key clash
     * occurs, or if another exception occurs.
     */
    public void addIdentity(Identity identity)
    throws KeyManagementException {
        localCheck("add.identity");
        Identity byName = getIdentity(identity.getName());
        Identity byKey = getIdentity(identity.getPublicKey());
        String msg = null;

        if (byName != null) {
            msg = "name conflict";
        }
        if (byKey != null) {
            msg = "key conflict";
        }
        if (msg != null) {
            throw new KeyManagementException(msg);
        }
        identities.put(identity.getName(), identity);
    }

    /**
     * Removes an identity to the database.
     */
    public void removeIdentity(Identity identity)
    throws KeyManagementException {
        localCheck("remove.identity");
        String name = identity.getName();
        if (identities.get(name) == null) {
            throw new KeyManagementException("there is no identity named " +
                                             name + " in " + this);
        }
        identities.remove(name);
    }

    /**
     * @return an enumeration of all identities in the database.
     */
    public Enumeration<Identity> identities() {
        return identities.elements();
    }

    /**
     * Set the source file for this database.
     */
    void setSourceFile(File f) {
        sourceFile = f;
    }

    /**
     * @return the source file for this database.
     */
    File getSourceFile() {
        return sourceFile;
    }

    /**
     * Save the database in its current state to an output stream.
     *
     * @param os the output stream to which the database should be serialized.
     *
     * @exception IOException if an IO exception is raised by stream
     * operations.
     */
    public void save(OutputStream os) throws IOException {
        try {
            ObjectOutputStream oos = new ObjectOutputStream(os);
            oos.writeObject(this);
            oos.flush();
        } catch (InvalidClassException e) {
            debug("This should not be happening.", e);
            return;
        }
    }

    /**
     * Save the database to a file.
     *
     * @exception IOException if an IO exception is raised by stream
     * operations.
     */
    void save(File f) throws IOException {
        setSourceFile(f);
        FileOutputStream fos = new FileOutputStream(f);
        save(fos);
    }

    /**
     * Saves the database to the default source file.
     *
     * @exception KeyManagementException when there is no default source
     * file specified for this database.
     */
    public void save() throws IOException {
        if (sourceFile == null) {
            throw new IOException("this database has no source file");
        }
        save(sourceFile);
    }

    /**
     * This method returns the file from which to initialize the
     * system database.
     */
    private static File systemDatabaseFile() {

        // First figure out where the identity database is hiding, if anywhere.
        String dbPath = Security.getProperty("identity.database");
        // if nowhere, it's the canonical place.
        if (dbPath == null) {
            dbPath = System.getProperty("user.home") + File.separatorChar +
                "identitydb.obj";
        }
        return new File(dbPath);
    }


    /* This block initializes the system database, if there is one. */
    static {
        java.security.AccessController.doPrivileged(
            new java.security.PrivilegedAction<Void>() {
            public Void run() {
                initializeSystem();
                return null;
            }
        });
    }

    /**
     * This method initializes the system's identity database. The
     * canonical location is
     * <user.home>/identitydatabase.obj. This is settable through
     * the identity.database property.  */
    private static void initializeSystem() {

        IdentityDatabase systemDatabase;
        File dbFile = systemDatabaseFile();

        // Second figure out if it's there, and if it isn't, create one.
        try {
            if (dbFile.exists()) {
                debug("loading system database from file: " + dbFile);
                systemDatabase = fromFile(dbFile);
            } else {
                systemDatabase = new IdentityDatabase(dbFile);
            }
            IdentityScope.setSystemScope(systemDatabase);
            debug("System database initialized: " + systemDatabase);
        } catch (IOException e) {
            debug("Error initializing identity database: " + dbFile, e);
            return;
        } catch (InvalidParameterException e) {
            debug("Error trying to instantiate a system identities db in " +
                               dbFile, e);
            return;
        }
    }

    /*
    private static File securityPropFile(String filename) {
        // maybe check for a system property which will specify where to
        // look.
        String sep = File.separator;
        return new File(System.getProperty("java.home") +
                        sep + "lib" + sep + "security" +
                        sep + filename);
    }
    */

    public String toString() {
        return "sun.security.provider.IdentityDatabase, source file: " +
            sourceFile;
    }


    private static void debug(String s) {
        if (debug) {
            System.err.println(s);
        }
    }

    private static void debug(String s, Throwable t) {
        if (debug) {
            t.printStackTrace();
            System.err.println(s);
        }
    }

    private static void error(String s) {
        if (error) {
            System.err.println(s);
        }
    }

    void localCheck(String directive) {
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            directive = this.getClass().getName() + "." +
                directive + "." + localFullName();
            security.checkSecurityAccess(directive);
        }
    }

    /**
     * Returns a parsable name for identity: identityName.scopeName
     */
    String localFullName() {
        String parsable = getName();
        if (getScope() != null) {
            parsable += "." +getScope().getName();
        }
        return parsable;
    }

    /**
     * Serialization write.
     */
    private synchronized void writeObject (java.io.ObjectOutputStream stream)
    throws IOException {
        localCheck("serialize.identity.database");
        stream.writeObject(identities);
        stream.writeObject(sourceFile);
    }
}
