/*-
 * See the file LICENSE for redistribution information.
 *
 * Copyright (c) 2004-2010 Oracle.  All rights reserved.
 *
 * $Id: ToManyExample.java,v 1.14 2010/01/04 15:50:34 cwl Exp $
 */

package je;

import java.io.File;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import com.sleepycat.bind.EntryBinding;
import com.sleepycat.bind.serial.SerialBinding;
import com.sleepycat.bind.serial.StoredClassCatalog;
import com.sleepycat.bind.tuple.StringBinding;
import com.sleepycat.je.Database;
import com.sleepycat.je.DatabaseConfig;
import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;
import com.sleepycat.je.ForeignKeyDeleteAction;
import com.sleepycat.je.ForeignMultiKeyNullifier;
import com.sleepycat.je.OperationStatus;
import com.sleepycat.je.SecondaryConfig;
import com.sleepycat.je.SecondaryCursor;
import com.sleepycat.je.SecondaryDatabase;
import com.sleepycat.je.SecondaryMultiKeyCreator;
import com.sleepycat.je.Transaction;

/**
 * An example of using many-to-many and one-to-many secondary indices.
 */
public class ToManyExample {

    private final Environment env;
    private Database catalogDb;
    private Database animalDb;
    private Database personDb;
    private SecondaryDatabase personByEmail;
    private SecondaryDatabase personByAnimal;
    private EntryBinding<String> keyBinding;
    private EntryBinding<Person> personBinding;
    private EntryBinding<Animal> animalBinding;

    /**
     * Runs the example program, given a single "-h HOME" argument.
     */
    public static void main(String[] args) {

        if (args.length != 2 || !"-h".equals(args[0])) {
            System.out.println("Usage: java " +
                               ToManyExample.class.getName() +
                               " -h ENV_HOME");
            System.exit(1);
        }
        String homeDir = args[1];

        try {
            ToManyExample example = new ToManyExample(homeDir);
            example.exec();
            example.close();
        } catch (DatabaseException e) {
            e.printStackTrace();
        }
    }

    /**
     * Opens the environment and all databases.
     */
    private ToManyExample(String homeDir) throws DatabaseException {

        /* Open the environment. */
        EnvironmentConfig envConfig = new EnvironmentConfig();
        envConfig.setAllowCreate(true);
        envConfig.setTransactional(true);
        env = new Environment(new File(homeDir), envConfig);

        /* Open/create all databases in a transaction. */
        Transaction txn = env.beginTransaction(null, null);
        try {
            /* A standard (no duplicates) database config. */
            DatabaseConfig dbConfig = new DatabaseConfig();
            dbConfig.setAllowCreate(true);
            dbConfig.setTransactional(true);

            /* The catalog is used for the serial binding. */
            catalogDb = env.openDatabase(txn, "catalog", dbConfig);
            StoredClassCatalog catalog = new StoredClassCatalog(catalogDb);
            personBinding = new SerialBinding(catalog, null);
            animalBinding = new SerialBinding(catalog, null);
            keyBinding = new StringBinding();

            /* Open the person and animal primary DBs. */
            animalDb = env.openDatabase(txn, "animal", dbConfig);
            personDb = env.openDatabase(txn, "person", dbConfig);

            /*
             * A standard secondary config; duplicates, key creators and key
             * nullifiers are specified below.
             */
            SecondaryConfig secConfig = new SecondaryConfig();
            secConfig.setAllowCreate(true);
            secConfig.setTransactional(true);

            /*
             * Open the secondary database for personByEmail.  This is a
             * one-to-many index because duplicates are not configured.
             */
            secConfig.setSortedDuplicates(false);
            secConfig.setMultiKeyCreator(new EmailKeyCreator());
            personByEmail = env.openSecondaryDatabase(txn, "personByEmail",
                                                      personDb, secConfig);

            /*
             * Open the secondary database for personByAnimal.  This is a
             * many-to-many index because duplicates are configured.  Foreign
             * key constraints are specified to ensure that all animal keys
             * exist in the animal database.
             */
            secConfig.setSortedDuplicates(true);
            secConfig.setMultiKeyCreator(new AnimalKeyCreator());
            secConfig.setForeignMultiKeyNullifier(new AnimalKeyNullifier());
            secConfig.setForeignKeyDatabase(animalDb);
            secConfig.setForeignKeyDeleteAction(ForeignKeyDeleteAction.NULLIFY);
            personByAnimal = env.openSecondaryDatabase(txn, "personByAnimal",
                                                       personDb, secConfig);

            txn.commit();
        } catch (DatabaseException e) {
            txn.abort();
            throw e;
        } catch (RuntimeException e) {
            txn.abort();
            throw e;
        }
    }

    /**
     * Closes all databases and the environment.
     */
    private void close() throws DatabaseException {

        if (personByEmail != null) {
            personByEmail.close();
        }
        if (personByAnimal != null) {
            personByAnimal.close();
        }
        if (catalogDb != null) {
            catalogDb.close();
        }
        if (personDb != null) {
            personDb.close();
        }
        if (animalDb != null) {
            animalDb.close();
        }
        if (env != null) {
            env.close();
        }
    }

    /**
     * Adds, updates, prints and deletes Person records with many-to-many and
     * one-to-many secondary indices.
     */
    private void exec()
        throws DatabaseException {

        System.out.println
            ("\nInsert some animals.");
        Animal dogs = insertAndPrintAnimal("dogs", true);
        Animal fish = insertAndPrintAnimal("fish", false);
        Animal horses = insertAndPrintAnimal("horses", true);
        Animal donkeys = insertAndPrintAnimal("donkeys", true);

        System.out.println
            ("\nInsert a new empty person.");
        Person kathy = new Person();
        kathy.name = "Kathy";
        putPerson(kathy);
        printPerson("Kathy");

        System.out.println
            ("\nAdd favorites/addresses and update the record.");
        kathy.favoriteAnimals.add(horses.name);
        kathy.favoriteAnimals.add(dogs.name);
        kathy.favoriteAnimals.add(fish.name);
        kathy.emailAddresses.add("kathy@kathy.com");
        kathy.emailAddresses.add("kathy@yahoo.com");
        putPerson(kathy);
        printPerson("Kathy");

        System.out.println
            ("\nChange favorites and addresses and update the person record.");
        kathy.favoriteAnimals.remove(fish.name);
        kathy.favoriteAnimals.add(donkeys.name);
        kathy.emailAddresses.add("kathy@gmail.com");
        kathy.emailAddresses.remove("kathy@yahoo.com");
        putPerson(kathy);
        printPerson("Kathy");

        System.out.println
            ("\nInsert another person with some of the same favorites.");
        Person mark = new Person();
        mark.favoriteAnimals.add(dogs.name);
        mark.favoriteAnimals.add(horses.name);
        mark.name = "Mark";
        putPerson(mark);
        printPerson("Mark");

        System.out.println
            ("\nPrint by favorite animal index.");
        printByIndex(personByAnimal);

        System.out.println
            ("\nPrint by email address index.");
        printByIndex(personByEmail);

        System.out.println
            ("\nDelete 'dogs' and print again by favorite animal index.");
        deleteAnimal(dogs.name);
        printPerson("Kathy");
        printPerson("Mark");
        printByIndex(personByAnimal);

        System.out.println
            ("\nDelete both records and print again (should print nothing).");
        deletePerson("Kathy");
        deletePerson("Mark");
        printPerson("Kathy");
        printPerson("Mark");
        printByIndex(personByAnimal);
        printByIndex(personByEmail);
    }

    /**
     * Inserts an animal record and prints it.  Uses auto-commit.
     */
    private Animal insertAndPrintAnimal(String name, boolean furry)
        throws DatabaseException {

        Animal animal = new Animal();
        animal.name = name;
        animal.furry = furry;

        DatabaseEntry key = new DatabaseEntry();
        keyBinding.objectToEntry(name, key);

        DatabaseEntry data = new DatabaseEntry();
        animalBinding.objectToEntry(animal, data);

        OperationStatus status = animalDb.putNoOverwrite(null, key, data);
        if (status == OperationStatus.SUCCESS) {
            System.out.println(animal);
        } else {
            System.out.println("Animal was not inserted: " + name +
                               " (" + status + ')');
        }

        return animal;
    }

    /**
     * Deletes an animal.  Uses auto-commit.
     */
    private boolean deleteAnimal(String name)
        throws DatabaseException {

        DatabaseEntry key = new DatabaseEntry();
        keyBinding.objectToEntry(name, key);

        OperationStatus status = animalDb.delete(null, key);
        return status == OperationStatus.SUCCESS;
    }

    /**
     * Gets a person by name and prints it.
     */
    private void printPerson(String name)
        throws DatabaseException {

        DatabaseEntry key = new DatabaseEntry();
        keyBinding.objectToEntry(name, key);

        DatabaseEntry data = new DatabaseEntry();

        OperationStatus status = personDb.get(null, key, data, null);
        if (status == OperationStatus.SUCCESS) {
            Person person = personBinding.entryToObject(data);
            person.name = keyBinding.entryToObject(key);
            System.out.println(person);
        } else {
            System.out.println("Person not found: " + name);
        }
    }

    /**
     * Prints all person records by a given secondary index.
     */
    private void printByIndex(SecondaryDatabase secDb)
        throws DatabaseException {

        DatabaseEntry secKey = new DatabaseEntry();
        DatabaseEntry priKey = new DatabaseEntry();
        DatabaseEntry priData = new DatabaseEntry();

        SecondaryCursor cursor = secDb.openSecondaryCursor(null, null);
        try {
            while (cursor.getNext(secKey, priKey, priData, null) ==
                   OperationStatus.SUCCESS) {
                Person person = personBinding.entryToObject(priData);
                person.name = keyBinding.entryToObject(priKey);
                System.out.println("Index key [" +
                                   keyBinding.entryToObject(secKey) +
                                   "] maps to primary key [" +
                                   person.name + ']');
            }
        } finally {
            cursor.close();
        }
    }

    /**
     * Inserts or updates a person.  Uses auto-commit.
     */
    private void putPerson(Person person)
        throws DatabaseException {

        DatabaseEntry key = new DatabaseEntry();
        keyBinding.objectToEntry(person.name, key);

        DatabaseEntry data = new DatabaseEntry();
        personBinding.objectToEntry(person, data);

        personDb.put(null, key, data);
    }

    /**
     * Deletes a person.  Uses auto-commit.
     */
    private boolean deletePerson(String name)
        throws DatabaseException {

        DatabaseEntry key = new DatabaseEntry();
        keyBinding.objectToEntry(name, key);

        OperationStatus status = personDb.delete(null, key);
        return status == OperationStatus.SUCCESS;
    }

    /**
     * A person object.
     */
    @SuppressWarnings("serial")
    private static class Person implements Serializable {

        /** The primary key. */
        private transient String name;

        /** A many-to-many set of keys. */
        private final Set<String> favoriteAnimals = new HashSet<String>();

        /** A one-to-many set of keys. */
        private final Set<String> emailAddresses = new HashSet<String>();

        @Override
        public String toString() {
            return "Person {" +
                   "\n  Name: " + name +
                   "\n  FavoriteAnimals: " + favoriteAnimals +
                   "\n  EmailAddresses: " + emailAddresses +
                   "\n}";
        }
    }

    /**
     * An animal object.
     */
    @SuppressWarnings("serial")
    private static class Animal implements Serializable {

        /** The primary key. */
        private transient String name;

        /** A non-indexed property. */
        private boolean furry;

        @Override
        public String toString() {
            return "Animal {" +
                   "\n  Name: " + name +
                   "\n  Furry: " + furry +
                   "\n}";
        }
    }

    /**
     * Returns the set of email addresses for a person.  This is an example
     * of a multi-key creator for a to-many index.
     */
    private class EmailKeyCreator implements SecondaryMultiKeyCreator {

        public void createSecondaryKeys(SecondaryDatabase secondary,
                                        DatabaseEntry primaryKey,
                                        DatabaseEntry primaryData,
                                        Set<DatabaseEntry> results) {
            Person person = personBinding.entryToObject(primaryData);
            copyKeysToEntries(person.emailAddresses, results);
        }
    }

    /**
     * Returns the set of favorite animals for a person.  This is an example
     * of a multi-key creator for a to-many index.
     */
    private class AnimalKeyCreator implements SecondaryMultiKeyCreator {

        public void createSecondaryKeys(SecondaryDatabase secondary,
                                        DatabaseEntry primaryKey,
                                        DatabaseEntry primaryData,
                                        Set<DatabaseEntry> results) {
            Person person = personBinding.entryToObject(primaryData);
            copyKeysToEntries(person.favoriteAnimals, results);
        }
    }

    /**
     * A utility method to copy a set of keys (Strings) into a set of
     * DatabaseEntry objects.
     */
    private void copyKeysToEntries(Set<String> keys,
                                   Set<DatabaseEntry> entries) {

        for (Iterator<String> i = keys.iterator(); i.hasNext();) {
            DatabaseEntry entry = new DatabaseEntry();
            keyBinding.objectToEntry(i.next(), entry);
            entries.add(entry);
        }
    }

    /**
     * Removes a given key from the set of favorite animals for a person.  This
     * is an example of a nullifier for a to-many index.  The nullifier is
     * called when an animal record is deleted because we configured this
     * secondary with ForeignKeyDeleteAction.NULLIFY.
     */
    private class AnimalKeyNullifier implements ForeignMultiKeyNullifier {

        public boolean nullifyForeignKey(SecondaryDatabase secondary,
                                         DatabaseEntry primaryKey,
                                         DatabaseEntry primaryData,
                                         DatabaseEntry secKey) {
            Person person = personBinding.entryToObject(primaryData);
            String key = keyBinding.entryToObject(secKey);
            if (person.favoriteAnimals.remove(key)) {
                personBinding.objectToEntry(person, primaryData);
                return true;
            } else {
                return false;
            }
        }
    }
}
