/*-
 *
 *  This file is part of Oracle NoSQL Database
 *  Copyright (C) 2011, 2014 Oracle and/or its affiliates.  All rights reserved.
 *
 *  Oracle NoSQL Database is free software: you can redistribute it and/or
 *  modify it under the terms of the GNU Affero General Public License
 *  as published by the Free Software Foundation, version 3.
 *
 *  Oracle NoSQL Database 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
 *  Affero General Public License for more details.
 *
 *  You should have received a copy of the GNU Affero General Public
 *  License in the LICENSE file along with Oracle NoSQL Database.  If not,
 *  see <http://www.gnu.org/licenses/>.
 *
 *  An active Oracle commercial licensing agreement for this product
 *  supercedes this license.
 *
 *  For more information please contact:
 *
 *  Vice President Legal, Development
 *  Oracle America, Inc.
 *  5OP-10
 *  500 Oracle Parkway
 *  Redwood Shores, CA 94065
 *
 *  or
 *
 *  berkeleydb-info_us@oracle.com
 *
 *  [This line intentionally left blank.]
 *  [This line intentionally left blank.]
 *  [This line intentionally left blank.]
 *  [This line intentionally left blank.]
 *  [This line intentionally left blank.]
 *  [This line intentionally left blank.]
 *  EOF
 *
 */

package avro;

import java.util.Arrays;

import oracle.kv.KVStore;
import oracle.kv.KVStoreConfig;
import oracle.kv.KVStoreFactory;
import oracle.kv.Key;
import oracle.kv.ValueVersion;
import oracle.kv.avro.AvroCatalog;
import oracle.kv.avro.SpecificAvroBinding;

/**
 * A simple KVStore client application that represents a {@code Value} as a
 * a POJO (or Plain Old Java Object) class that is generated by the Avro
 * compiler tools and serializes values using a {@link SpecificAvroBinding}.
 * <p>
 * An Ant build file, generate-specific.xml, is included with this example to
 * generate the specific class source files: MemberInfo.java, FullName.java and
 * Address.java.  When running "ant -f generate-specific.xml" (part of the
 * build instructions below), Ivy will be used to download the Avro jars
 * that are needed from the central Maven repository.  These jar files are
 * placed in the generate-specific-libs directory.  In a real application, the
 * contents of generate-specific.xml could be included in the application's Ant
 * build file, and the libraries in generate-specific-libs could be obtained
 * differently.
 * <p>
 * When using an Avro binding, the Avro and Jackson jars must be in the
 * classpath, as well as the kvclient jar.  The Avro and Jackson jars are
 * included in the KVHOME/lib directory along with the kvclient jar:
 * <pre>
 *    kvclient.jar avro.jar jackson-core-asl.jar jackson-mapper-asl.jar
 * </pre>
 * As long as all four jars are in the same directory, only the kvclient jar
 * needs to be specified in the classpath, because the kvclient jar references
 * the other three jars.  If they are not in the same directory, all four
 * jars must be explicitly specified in the classpath.
 * <p>
 * To build this example in the examples/avro directory:
 * <pre>
 *   cd KVHOME/examples/avro
 *   ant -f generate-specific.xml
 *   mkdir classes
 *   javac -cp KVHOME/lib/kvclient.jar -d classes *.java
 * </pre>
 * <p>
 * You'll noticed that the <code>ant -f generate-specific.xml</code> command
 * generates three source files in this directory: Address.java,
 * MemberInfo.java and FullName.java.  It is useful to look at these files to
 * understand how they are used, but they should not be modified directly.
 * <p>
 * Before running this example program, start a KVStore instance.  The simplest
 * way to do that is to run KV Lite as described in the Quickstart document.
 * <p>
 * After starting the KVStore instance, the Avro schema used by the example
 * must be added to the store using the administration command line interface
 * (CLI).  First start the admin CLI as described in the Oracle NoSQL Database
 * Administrator's Guide. Then enter the following command to add the example
 * schema:
 *  <pre>ddl add-schema -file member-schemas.avsc</pre>
 *
 * After adding the schema, use the KVStore instance name, host and port for
 * running this program, as follows:
 *
 * <pre>
 * java -cp classes:KVHOME/lib/kvclient.jar avro.SpecificExample \
 *                       -store &lt;instance name&gt; \
 *                       -host  &lt;host name&gt;     \
 *                       -port  &lt;port number&gt;
 * </pre>
 *
 * For all examples the default instance name is kvstore, the default host name
 * is localhost and the default port number is 5000.  These defaults match the
 * defaults for running kvlite, so the simplest way to run the examples along
 * with kvlite is to omit all parameters.
 * <p>
 * In this example a single key is used for storing a kv pair, where the value
 * is an object serialized as Avro binary data.  The first time the example is
 * run it inserts the kv pair, and subsequent times that it is run it reads and
 * updates the kv pair, incrementing the "age" field.
 * <p>
 * This example may also be used to demonstrate simple schema evolution by
 * performing the following steps:
 * <ol>
 *   <li>
 *   Build and run the example as described above.
 *   </li>
 *   <li>
 *   Change the FullName schema in member-schemas.avsc to add the "middle"
 *   field, including a default value:
 *   <pre>
 *      "fields": [
 *          {"name": "first", "type": "string", "default": ""},
 *          {"name": "middle", "type": "string", "default": ""},
 *          {"name": "last", "type": "string", "default": ""}
 *      ]
 *   </pre>
 *   </li>
 *   <li>
 *   Uncomment the two lines in the example that set the middle name field.  In
 *   other words, change:
 *   <pre>
 *          //final FullName name = member.getName();
 *          //name.setMiddle("Lawrence");
 *   </pre>
 *   to:
 *   <pre>
 *          final FullName name = member.getName();
 *          name.setMiddle("Lawrence");
 *   </pre>
 *   </li>
 *   <li>
 *   Build and run the modified example as before.  Be sure to re-run the
 *   <code>ant -f generate-specific.xml</code> command to re-generate the three
 *   source files and re-compile all source files with <code>javac</code>.
 *   <p>
 *   The -evolve option must be used with the add-schema command as follows:
 *   <pre>ddl add-schema -file member-schemas.avsc -evolve</pre>
 *   Finally, run the example as before.
 *   </li>
 * </ol>
 * <p>
 * When the example is run after being changed, the initial value that is read
 * (which was stored with the old version of the schema) will be displayed with
 * a middle name field with an empty string value.  This is because, when Avro
 * deserializes a stored value that has no middle name field, it adds the
 * middle name field because it is present in the reader schema, and it assigns
 * it the default value for that field.  A default value is required whenever a
 * new field is added.
 * <p>
 * The updated example will also set the middle name to "Lawrence" when it
 * increments the age field and stores the updated value.  So the final value
 * displayed will include this updated value for the middle name.
 * <p>
 * You can also reverse this process to demonstrate what happens when a client
 * using the old schema reads and writes a kv pair that was written earlier
 * using the new schema.  To try this, undo the two changes above -- remove the
 * middle name field from the schema and comment out the two lines that set the
 * middle name value -- and then build and run the program.
 * <p>
 * When the example is run after undoing these changes, the initial value that
 * is read (which was stored with the new version of the schema) will be
 * displayed without a middle name field.  This is because, when Avro
 * deserializes a stored value that has a middle name, it ignores it because no
 * middle name field is present in the reader schema.
 * <p>
 * Note that when this version of the example program updates the age field and
 * writes the modified record, no middle name is included in the stored data.
 * In other words, the middle name that was stored earlier is lost.
 * <p>
 * Also note that the same rules apply when a field is intentionally deleted
 * in a new version of the schema because it is no longer needed.  From the
 * Avro perspective, there is no difference between the two scenarios.
 */
public class SpecificExample {

    private final KVStore store;
    private final AvroCatalog catalog;
    private final SpecificAvroBinding<MemberInfo> binding;

    /**
     * Runs the SpecificExample command line program.
     */
    public static void main(String args[]) {
        try {
            SpecificExample example = new SpecificExample(args);
            example.runExample();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Parses the command line args, opens the KVStore and creates the Avro
     * binding.
     */
    SpecificExample(String[] argv) {

        String storeName = "kvstore";
        String hostName = "localhost";
        String hostPort = "5000";

        final int nArgs = argv.length;
        int argc = 0;

        while (argc < nArgs) {
            final String thisArg = argv[argc++];

            if (thisArg.equals("-store")) {
                if (argc < nArgs) {
                    storeName = argv[argc++];
                } else {
                    usage("-store requires an argument");
                }
            } else if (thisArg.equals("-host")) {
                if (argc < nArgs) {
                    hostName = argv[argc++];
                } else {
                    usage("-host requires an argument");
                }
            } else if (thisArg.equals("-port")) {
                if (argc < nArgs) {
                    hostPort = argv[argc++];
                } else {
                    usage("-port requires an argument");
                }
            } else {
                usage("Unknown argument: " + thisArg);
            }
        }

        /* Open the KVStore. */
        store = KVStoreFactory.getStore
            (new KVStoreConfig(storeName, hostName + ":" + hostPort));

        /* Create a specific binding for the MemberInfo class/schema. */
        catalog = store.getAvroCatalog();
        binding = catalog.getSpecificBinding(MemberInfo.class);
    }

    private void usage(String message) {
        System.out.println("\n" + message + "\n");
        System.out.println("usage: " + getClass().getName());
        System.out.println("\t-store <instance name> (default: kvstore) " +
                           "-host <host name> (default: localhost) " +
                           "-port <port number> (default: 5000)");
        System.exit(1);
    }

    /**
     * Insert a kv pair if it doesn't exist, or read/update it if it does.
     */
    void runExample() {

        /* Use key "/mb/0000000001" to store the member object. */
        final Key key = Key.createKey(Arrays.asList("mb", "0000000001"));

        /* Read the value we previous stored, if any. */
        final ValueVersion valueVersion = store.get(key);
        final MemberInfo member;
        final int age;
        if (valueVersion != null) {

            /* Deserialize the value. */
            member = binding.toObject(valueVersion.getValue());

            /* Print it as a JSON string. */
            System.out.println("INITIAL VALUE:\n" + member.toString());

            /* Increment age field. */
            final int oldAge = member.getAge();
            age = oldAge + 1;
            member.setAge(age);

            /*
             * Fill in the middle name when the schema is upgraded. These lines
             * are initially commented out because the middle name field is not
             * present in the schema.  They can be uncommented when the middle
             * name field is added to the schema.
             */
            //final FullName name = member.getName();
            //name.setMiddle("Lawrence");
        } else {
            System.out.println("NO INITIAL VALUE");

            /* Create a fresh MemberInfo object. */
            member = createMember();
            age = member.getAge();
        }

        /* Serialize the specific record and write it. */
        store.put(key, binding.toValue(member));

        /* Read it again to confirm that it was stored. */
        final ValueVersion valueVersion2 = store.get(key);
        final MemberInfo member2 = binding.toObject(valueVersion2.getValue());

        /* Check for expected age. */
        final int age2 = member2.getAge();
        if (age2 != age) {
            throw new RuntimeException("Expected: " + age +
                                       " but got: " + age2);
        }

        /* Print object as a JSON string. */
        System.out.println("FINAL VALUE:\n" + member2.toString());

        store.close();
    }

    /**
     * Uses the generated classes to create a MemberInfo instance that conforms
     * to the MemberInfo schema.
     */
    private MemberInfo createMember() {

        final FullName name = new FullName();
        name.setFirst("Percival");
        name.setLast("Lowell");

        final Address addr = new Address();
        addr.setStreet("Mars Hill Rd");
        addr.setCity("Flagstaff");
        addr.setState("AZ");
        addr.setZip(86001);

        final MemberInfo member = new MemberInfo();
        member.setName(name);
        member.setAge(156);
        member.setAddress(addr);

        return member;
    }
}
