All Downloads are FREE. Search and download functionalities are using the official Maven repository.

oracle.kv.util.shell.LoadTableUtils Maven / Gradle / Ivy

Go to download

NoSQL Database Server - supplies build and runtime support for the server (store) side of the Oracle NoSQL Database.

The newest version!
/*-
 * Copyright (C) 2011, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This file was distributed by Oracle as part of a version of Oracle NoSQL
 * Database made available at:
 *
 * http://www.oracle.com/technetwork/database/database-technologies/nosqldb/downloads/index.html
 *
 * Please see the LICENSE file included in the top-level directory of the
 * appropriate version of Oracle NoSQL Database for a copy of the license and
 * additional information.
 */

package oracle.kv.util.shell;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import oracle.kv.FaultException;
import oracle.kv.impl.api.table.FieldDefImpl;
import oracle.kv.table.FieldDef;
import oracle.kv.table.FieldValue;
import oracle.kv.table.Row;
import oracle.kv.table.Table;
import oracle.kv.table.TableAPI;
import oracle.kv.table.WriteOptions;

/**
 * A utility class that encapsulates functions to load records from a file to
 * table(s), the support formats of record are JSON and CSV.
 *
 * The target table to be loaded into can be specified either by the input
 * argument, or by adding a comment line "Table: " ahead of  the
 * records to load in the file, see below example:
 *
 * e.g. 2 tables and their records included in data.txt file.
 *
 * Table: users
 * 
 * ...
 *
 * Table: emails
 * 
 * ...
 *
 * The table name specified in the file take the precedence to be used.
 */
public class LoadTableUtils {

    private final static char LEFT_CURLY_BRACE = '{';
    private final static char RIGHT_CURLY_BRACE = '}';
    private final static char DOUBLE_QUOTE = '\"';
    private final static char ESCAPE = '\\';

    private static enum Type {JSON, CSV}

    /**
     * Load JSONs from a file to table(s), the target table(s) should be
     * specified in the file with a line of "Table: " before records of
     * the table.
     *
     * @param tableImpl the TableAPI interface for the store.
     * @param fileName the file that contains JSON records.
     * @param options the write options used for put operation.
     *
     * @return a map of table name and its corresponding record count loaded.
     */
    public static Map loadJsonFromFile(final TableAPI tableImpl,
                                                     final String fileName,
                                                     final WriteOptions options)
        throws IllegalArgumentException, IOException, FaultException {

        return loadJsonFromFile(tableImpl, null, fileName, options);
    }

    /**
     * Load JSONs from a file to the specified table.
     *
     * A overloaded {@link #loadJsonFromFile(TableAPI, String, WriteOptions)}
     * method, the difference is that 2nd argument "table" is provided to
     * specify the target table to load records to.
     */
    public static Map loadJsonFromFile(final TableAPI tableImpl,
                                                     final Table table,
                                                     final String fileName,
                                                     final WriteOptions options)
        throws IllegalArgumentException, IOException, FaultException {

        return new Loader(tableImpl).loadJsonToTables(table, fileName,
                                                      options, true);
    }

    /**
     * Load CSVs from a file to table(s), the target table(s) should be
     * specified in the file with a line of "Table: " before records of
     * the table.
     *
     * @param tableImpl the TableAPI interface for the store.
     * @param fileName the file that contains CSV records to load.
     * @param options the write options used for put operation.
     *
     * @return a map of table name and its corresponding record count loaded.
     */
    public static Map loadCSVFromFile(final TableAPI tableImpl,
                                                    final String fileName,
                                                    final WriteOptions options)
        throws IllegalArgumentException, IOException, FaultException {

        return loadCSVFromFile(tableImpl, null, fileName, options);
    }

    /**
     * Load JSONs from a file to the specified table.
     *
     * A overloaded {@link #loadCSVFromFile(TableAPI, String, WriteOptions)}
     * method, the difference is that 2nd argument "table" is provided to
     * specify the target table to load records to.
     */
    public static Map loadCSVFromFile(final TableAPI tableImpl,
                                                    final Table table,
                                                    final String fileName,
                                                    final WriteOptions options)
        throws IllegalArgumentException, IOException, RuntimeException {

        return new Loader(tableImpl).loadCSVToTables(table, fileName,
                                                     options, true);
    }

    /**
     * The loader class implements methods to load JSON or CSV records from a
     * file to table(s).
     */
    public static class Loader {

        private final TableAPI tableImpl;

        public Loader(final TableAPI tableImpl) {
            this.tableImpl = tableImpl;
        }

        /**
         * Load JSON records from a file to tables.
         *
         * @param table the initial table to which JSON records are loaded.
         * @param fileName the file contains JSON records.
         * @param options the WriteOptions used to put records.
         * @param exitOnFailure the flag indicates if exits if a record is
         *        failed to put.
         *
         * @return A map of table name and count of records loaded.
         */
        public Map loadJsonToTables(Table table,
                                                  String fileName,
                                                  WriteOptions options,
                                                  boolean exitOnFailure)
            throws IllegalArgumentException, IOException, FaultException {

            return loadRecordsFromFile(table, fileName, Type.JSON,
                                       options, false, exitOnFailure);
        }

        /**
         * Load JSON records from a file to the specified table.
         *
         * @param table the target table to which JSON records are loaded, the
         *        records of other tables will be skipped.
         * @param fileName the file contains JSON records.
         * @param options the WriteOptions used to put records.
         * @param exitOnFailure the flag indicates if exits if a record is
         *        failed to put.
         *
         * @return The total number of records loaded to the target table.
         */
        public long loadJsonToTable(Table table,
                                    String fileName,
                                    WriteOptions options,
                                    boolean exitOnFailure)
            throws IllegalArgumentException, IOException, FaultException {

            Map results = loadRecordsFromFile(table,
                                                            fileName,
                                                            Type.JSON,
                                                            options,
                                                            true,
                                                            exitOnFailure);
            if (results.isEmpty()) {
                return 0;
            }
            final String tableName = table.getFullName();
            assert(results.containsKey(tableName));
            return results.get(tableName);
        }

        /**
         * Load CSV records from a file to the specified table.
         *
         * @param table the target table to which CSV records are loaded, the
         *        records of other tables will be skipped.
         * @param fileName the file contains CSV records.
         * @param options the WriteOptions used to put records.
         * @param exitOnFailure the flag indicates if exits if a record is
         *        failed to put.
         *
         * @return A map of table name and count of records loaded.
         */
        public Map loadCSVToTables(Table table,
                                                 String fileName,
                                                 WriteOptions options,
                                                 boolean exitOnFailure)
            throws IllegalArgumentException, IOException, FaultException {

            return loadRecordsFromFile(table, fileName, Type.CSV,
                                       options, false, exitOnFailure);
        }

        /**
         * Load records from a file to table(s)
         *
         * @param table the target table to which the records are loaded.
         * @param fileName the file that contains the records.
         * @param type the type of record: JSON or CSV.
         * @param options the WriteOptions used to performance put operation.
         * @param targetTableOnly a flag to indicate that only load record to
         *        the specified target {@code table}, the records for other
         *        tables are skipped.
         * @param exitOnFailure a flag to indicate if terminate the load process
         *        if any failure.
         *
         * @return A map of table name and count of records loaded.
         *
         * @throws IOException
         * @throws RuntimeException
         */
        private Map
            loadRecordsFromFile(final Table table,
                                final String fileName,
                                final Type type,
                                final WriteOptions options,
                                final boolean targetTableOnly,
                                final boolean exitOnFailure)
            throws IllegalArgumentException, IOException, FaultException {

            assert (!targetTableOnly || table != null);

            if (!new File(fileName).exists()) {
                throw new FileNotFoundException("File not found: " + fileName);
            }

            if (type == Type.CSV) {
                for (String fname : table.getFields()) {
                    final FieldDef fdef = table.getField(fname);
                    if (fdef.isComplex()) {
                        final String fmt = "The table \"%s\" contains a complex " +
                            "field \"%s\" that cannot be imported as CSV format";
                        throw new IllegalArgumentException(
                            String.format(fmt, table.getFullName(), fname));
                    }
                }
            }

            BufferedReader br = null;
            try {
                Table target = table;
                Row row = null;
                final StringBuilder buffer = new StringBuilder();
                String line;
                long nLoaded = 0;
                long nLine = 0;
                int nCurlyBrace = 0;
                boolean skipLine = false;
                Map result = new HashMap();
                br = new BufferedReader(new FileReader(fileName));

                while((line = br.readLine()) != null) {
                    nLine++;

                    /* Skip comment line or empty line. */
                    if (isComment(line)) {
                        continue;
                    }

                    /*
                     * Check if the line includes table specification with the
                     * format: Table: .
                     */
                    final String name = parseTableName(line);
                    if (name != null) {
                        if (targetTableOnly) {
                            skipLine =
                                !target.getFullName().equalsIgnoreCase(name);
                        } else {
                            Table newTable = tableImpl.getTable(name);
                            if (newTable != null) {
                                if (target != null) {
                                    tallyCount(result, target, nLoaded);
                                    nLoaded = 0;
                                }
                                target = newTable;
                                skipLine = false;
                            } else {
                                if (exitOnFailure) {
                                    final String fmt = "Table %s (at line %s)" +
                                        " not found.";
                                    throw new IllegalArgumentException(
                                        String.format(fmt, name, nLine));
                                }
                                skipLine = true;
                            }
                        }
                        continue;
                    }

                    if (skipLine) {
                        continue;
                    }

                    if (target == null) {
                        final String fmt = "Missing table specification " +
                            "\"Table: \" before record at line %d of %s";
                        throw new IllegalArgumentException(
                            String.format(fmt, nLine, fileName));
                    }

                    String rowLine;
                    if (type == Type.JSON) {
                        buffer.append(line);
                        /*
                         * If curly braces are matched in pairs, then the JSON
                         * record is considered as completed one.
                         */
                        nCurlyBrace += getUnmatchedCurlyBraces(line);
                        if (nCurlyBrace != 0) {
                            continue;
                        }
                        rowLine = buffer.toString().trim();
                        if (!isValidJsonString(rowLine)) {
                            if (exitOnFailure) {
                                final String fmt = "Invalid JSON string at " +
                                		"line %d of %s: %s";
                                throw new IllegalArgumentException(
                                    String.format(fmt, nLine, fileName,
                                                  rowLine));
                            }
                        }
                        buffer.setLength(0);
                    } else {
                        rowLine = line;
                    }

                    try {
                        row = createRow(target, rowLine, type);
                        if (doPut(tableImpl, row, options)) {
                            ++nLoaded;
                        } else {
                            if (exitOnFailure) {
                                break;
                            }
                        }
                    } catch (RuntimeException rte) {
                        if (exitOnFailure) {
                            final String fmt = "Failed to import JSON row at " +
                                "line %d of file, %s: " + rte.getMessage();
                            throw new RuntimeException(
                                String.format(fmt, nLine, fileName), rte);
                        }
                        /* Ignore the exception if exitOnFailure is false. */
                    }
                }

                /* Handle the left string in buffer which is not a valid JSON */
                if (buffer.length() > 0) {
                    assert(type == Type.JSON);
                    if (exitOnFailure) {
                        throw new IllegalArgumentException
                        ("Invalid JSON string: " + buffer.toString());
                    }
                }

                if (target != null) {
                    tallyCount(result, target, nLoaded);
                }
                return result;
            } finally {
                if (br != null) {
                    try {
                        br.close();
                    } catch (IOException ignored) {
                    }
                }
            }
        }

        /**
         * Creates a row from a string.
         */
        private Row createRow(Table table, String str, Type type) {
            if (type == Type.JSON) {
                return createRowFromJson(table, str);
            }
            return createRowFromCSV(table, str);
        }

        /**
         * Creates row from JSON doc for the specified table.
         */
        public Row createRowFromJson(Table table, String str) {

            return table.createRowFromJson(str, false);
        }

        /**
         * Creates row from CVS record for the specified table.
         */
        public Row createRowFromCSV(Table table, String str) {
            final String[] values = splitCSVValues(str);
            final List fnames = table.getFields();
            if (values.length != fnames.size()) {
                final String fmt = "Invalid record for table %s, the number " +
                    "of values is expected to be %d but %d: %s";
                throw new IllegalArgumentException(
                    String.format(fmt, table.getFullName(), fnames.size(),
                                  values.length, str));
            }

            final Row row = table.createRow();
            for (int i = 0; i < values.length; i++) {
                String sval = values[i];
                final String fname = fnames.get(i);
                final FieldDef fdef = table.getField(fname);

                assert(!fdef.isComplex());

                if (isCSVNullValue(sval)) {
                    row.putNull(fname);
                } else {
                    /* Trim double quotes for string type value */
                    if (fdef.isString() || fdef.isEnum()) {
                        sval = trimQuotes(sval);
                    }
                    final FieldValue fval =
                        FieldDefImpl.createValueFromString(sval, fdef);
                    row.put(fname, fval);
                }
            }
            return row;
        }

        /**
         * Executes put operation.
         */
        public boolean doPut(TableAPI tableAPIImpl, Row row, WriteOptions wro) {
            return (tableAPIImpl.put(row, null, wro) != null);
        }

        /**
         * Tally the count of records for a table.
         */
        private static void tallyCount(final Map result,
                                       final Table table,
                                       final long count) {
            if (table == null || count == 0) {
                return;
            }
            final String name = table.getFullName();
            if (result.containsKey(name)) {
                long total = result.get(name) + count;
                result.put(name, total);
            } else {
                result.put(name, count);
            }
        }

        /**
         * Returns true if the line is comment line or empty line.
         */
        private boolean isComment(String line) {
            final String str = line.trim();
            if (str.length() == 0) {
                return true;
            }

            final int commentIdx = str.indexOf(Shell.COMMENT_MARK);
            if (commentIdx == 0) {
                return true;
            }
            return false;
        }

        /**
         * Parses the table name from the comment line, the format of
         * table specification is "Table: ", e.g. Table: users
         */
        private static String parseTableName(final String line) {
            final String[] tokens = {"table", ":"};
            String str = line.trim();
            for (String token : tokens) {
                if (!str.toLowerCase().startsWith(token)) {
                    return null;
                }
                str = str.substring(token.length()).trim();
            }
            return str;
        }


        /**
         * Returns true if the string value represents null value in CSV format
         */
        private static boolean isCSVNullValue(final String value) {
            return value.isEmpty();
        }

        /**
         * Split the line into token by comma but ignoring the embedded comma in
         * quotes.
         */
        private static String[] splitCSVValues(String line) {
            final String tline = line.trim();
            String[] values = tline.split(",(?=([^\"]*\"[^\"]*\")*[^\"]*$)", -1);
            for (int i = 0; i < values.length; i++) {
                values[i] = values[i].trim();
            }
            return values;
        }

        /**
         * Trim leading and trailing double quote of a string
         */
        private static String trimQuotes(final String value) {
            String str = value.trim();
            if (str.startsWith("\"") && str.endsWith("\"")) {
                str = str.substring(1, str.length() - 1);
            }
            return str;
        }

       /**
        * Return the number of unmatched curly braces. If unmatched curly
        * brace(s) are left brace(s), then return a positive number. If
        * unmatched curly brace(s) are right brace(s), then return
        * negative number.
        */
        private static int getUnmatchedCurlyBraces(String line) {
            int cnt = 0;
            boolean inQuotes = false;
            char prev = 0;
            for (int i = 0; i < line.length(); i++) {
                final char ch = line.charAt(i);
                switch (ch) {
                    case LEFT_CURLY_BRACE:
                        if (!inQuotes) {
                            cnt++;
                        }
                        break;
                    case RIGHT_CURLY_BRACE:
                        if (!inQuotes) {
                            cnt--;
                        }
                        break;
                    case DOUBLE_QUOTE:
                        if (prev != ESCAPE) {
                            inQuotes = !inQuotes;
                        }
                        break;
                    default:
                        break;
                }
                prev = ch;
            }
            return cnt;
        }

        /**
         * Check if the given string is valid JSON string.
         *
         * 1.JSON string should start with left curly brace '{' and end with
         *   the right curly brace '}'.
         */
        private boolean isValidJsonString(final String str) {
            assert(str != null && !str.isEmpty());

            if (str.indexOf(LEFT_CURLY_BRACE) != 0) {
                return false;
            }
            if (str.lastIndexOf(RIGHT_CURLY_BRACE) !=  str.length() - 1) {
                return false;
            }
            return true;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy