com.cinchapi.concourse.importer.util.Importables.java Source code

Java tutorial

Introduction

Here is the source code for com.cinchapi.concourse.importer.util.Importables.java

Source

/*
 * Copyright (c) 2013-2016 Cinchapi Inc.
 * 
 * Licensed 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 com.cinchapi.concourse.importer.util;

import java.io.IOException;
import java.util.Collection;
import java.util.List;

import javax.annotation.Nullable;

import org.apache.commons.lang.StringUtils;

import com.cinchapi.concourse.importer.Transformer;
import com.cinchapi.concourse.util.KeyValue;
import com.cinchapi.concourse.util.QuoteAwareStringSplitter;
import com.cinchapi.concourse.util.SplitOption;
import com.cinchapi.concourse.util.StringBuilderWriter;
import com.cinchapi.concourse.util.StringSplitter;
import com.cinchapi.concourse.util.Strings;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import com.google.gson.stream.JsonWriter;

/**
 * Helper functions to aid in the process of importing data from files or
 * strings.
 * 
 * @author Jeff Nelson
 */
public class Importables {

    /**
     * Given a string of {@code lines}, where is line has tokens that are
     * separated by a {@code delimiter} character, covert each line to a JSON
     * object and return a single string that contains a JSON array of the
     * objects.
     * 
     * @param lines the data to convert
     * @param resolveKey a key that the
     *            {@link com.cinchapi.concourse.importer.Importer importer} uses
     *            to determine the existing records into which the data should
     *            be imported. The method places the appropriate resolve
     *            instruction in the JSON blob so Concourse Server can perform
     *            the resolution during the import
     * @param delimiter the delimiter character
     * @param header the list of header keys. If this list is empty, it is
     *            assumed that the first line of {@code lines} contains the
     *            header
     * @param transformer the {@link Transformer} that is used to potentially
     *            alter key/value pairs before import
     * @return a JSON string that contains the raw {@code lines} in the new
     *         format
     * @throws IndexOutOfBoundsException if a line has more columns that there
     *             are header keys; it is fine for a line to have fewer columns
     *             that header keys
     */
    public static String delimitedStringToJsonArray(String lines, @Nullable String resolveKey, char delimiter,
            List<String> header, @Nullable Transformer transformer) {
        header = header == null ? Lists.<String>newArrayList() : header;
        StringSplitter it = createStringSplitter(lines, delimiter);
        StringBuilderWriter out = new StringBuilderWriter();
        int hcount = 0;
        try (JsonWriter json = new JsonWriter(out)) {
            json.beginArray();
            while (it.hasNext()) {
                if (header.isEmpty()) {
                    do {
                        header.add(it.next());
                    } while (it.hasNext() && !it.atEndOfLine());
                } else {
                    json.beginObject();
                    hcount = 0;
                    do {
                        String key = header.get(hcount);
                        ++hcount;
                        String value = it.next();
                        Object jvalue = value;
                        KeyValue<String, Object> kv = transformer == null ? null
                                : transformer.transform(key, value);
                        if (kv != null) {
                            key = kv.getKey();
                            jvalue = kv.getValue();
                            value = jvalue.toString();
                        }
                        // TODO process resolve key
                        json.name(key);
                        if (StringUtils.isBlank(value)) {
                            json.nullValue();
                        } else if (jvalue instanceof Collection) {
                            json.beginArray();
                            for (Object jitem : (Collection<?>) jvalue) {
                                writeJsonValue(json, jitem.toString());
                            }
                            json.endArray();
                        } else {
                            writeJsonValue(json, value);
                        }
                    } while (it.hasNext() && !it.atEndOfLine());
                    json.endObject();
                }
            }
            json.endArray();
            return out.toString();
        } catch (IOException e) {
            throw Throwables.propagate(e);
        }
    }

    /**
     * Given a {@code line} that is separated by a {@code delimiter} character,
     * convert each token to fields within a single JSON object.
     * 
     * @param line the data to convert
     * @param resolveKey a key that the
     *            {@link com.cinchapi.concourse.importer.Importer importer} uses
     *            to determine the existing records into which the data should
     *            be imported. The method places the appropriate resolve
     *            instruction in the JSON blob so Concourse Server can perform
     *            the resolution during the import
     * @param delimiter the delimiter character
     * @param header the list of header keys. If this list is empty, this method
     *            will return {@code null} and instead place the tokens from the
     *            {@code line} into the list
     * @param transformer the {@link Transformer} that is used to potentially
     *            alter key/value pairs before import
     * @return a JSON string that contains the raw {@code line} in the new
     *         format or {@code null} if {@code header} is
     *         {@link List#isEmpty() empty}
     * @throws IndexOutOfBoundsException if the line has more columns that there
     *             are header keys (as long as the number of header keys is >
     *             0); it is fine for a line to have fewer columns that header
     *             keys
     */
    @Nullable
    public static String delimitedStringToJsonObject(String line, @Nullable String resolveKey, char delimiter,
            List<String> header, @Nullable Transformer transformer) {
        StringBuilder builder = new StringBuilder();
        delimitedStringToJsonObject(line, resolveKey, delimiter, header, transformer, builder);
        return builder.length() == 0 ? null : builder.toString();
    }

    /**
     * Given a {@code line} that is separated by a {@code delimiter} character,
     * convert each token to fields within a single JSON object.
     * <p>
     * This method does not return anything. It writes the JSON blob to the
     * provided {@code builder}.
     * </p>
     * 
     * @param line the data to convert
     * @param resolveKey a key that the
     *            {@link com.cinchapi.concourse.importer.Importer importer} uses
     *            to determine the existing records into which the data should
     *            be imported. The method places the appropriate resolve
     *            instruction in the JSON blob so Concourse Server can perform
     *            the resolution during the import
     * @param delimiter the delimiter character
     * @param header the list of header keys. If this list is empty, this method
     *            will return {@code null} and instead place the tokens from the
     *            {@code line} into the list
     * @param transformer the {@link Transformer} that is used to potentially
     *            alter key/value pairs before import
     * @param builder the {@link StringBuilder} where the JSON blob should be
     *            written
     * @throws IndexOutOfBoundsException if the line has more columns that there
     *             are header keys (as long as the number of header keys is >
     *             0); it is fine for a line to have fewer columns that header
     *             keys
     */
    public static void delimitedStringToJsonObject(String line, @Nullable String resolveKey, char delimiter,
            List<String> header, @Nullable Transformer transformer, StringBuilder builder) {
        StringSplitter it = createStringSplitter(line, delimiter);
        if (header.isEmpty()) {
            while (it.hasNext()) {
                header.add(it.next());
            }
        } else {
            int hcount = 0;
            StringBuilderWriter out = new StringBuilderWriter(builder);
            try (JsonWriter json = new JsonWriter(out)) {
                json.beginObject();
                while (it.hasNext()) {
                    String key = header.get(hcount);
                    ++hcount;
                    String value = it.next();
                    Object jvalue = value;
                    KeyValue<String, Object> kv = transformer == null ? null : transformer.transform(key, value);
                    if (kv != null) {
                        key = kv.getKey();
                        jvalue = kv.getValue();
                        value = jvalue.toString();
                    }
                    // TODO process resolve key
                    json.name(key);
                    if (StringUtils.isBlank(value)) {
                        json.nullValue();
                    } else if (jvalue instanceof Collection) {
                        json.beginArray();
                        for (Object jitem : (Collection<?>) jvalue) {
                            writeJsonValue(json, jitem.toString());
                        }
                        json.endArray();
                    } else {
                        writeJsonValue(json, value);
                    }
                }
                json.endObject();
            } catch (IOException e) {
                throw Throwables.propagate(e);
            }
        }
    }

    /**
     * Intelligently write the appropriate JSON representation for {@code value}
     * to {@code out}.
     * 
     * @param out the {@link JsonWriter} to use for writing
     * @param value the value to write
     * @throws IOException
     */
    @VisibleForTesting
    protected static void writeJsonValue(JsonWriter out, String value) throws IOException {
        Object parsed;
        if ((parsed = Strings.tryParseNumberStrict(value)) != null) {
            out.value((Number) parsed);
        } else if ((parsed = Strings.tryParseBoolean(value)) != null) {
            out.value((boolean) parsed);
        } else {
            value = Strings.ensureWithinQuotes(value);
            value = Strings.escapeInner(value, value.charAt(0), '\n');
            out.jsonValue(value);
        }
    }

    /**
     * Return a {@link StringSplitter} for {@code string} and {@code delimiter}
     * with all the appropriate {@link SplitOption options}.
     * 
     * @param string the string over which to split
     * @param delimiter the delimiter on which to split
     * @return an appropriately configured {@link StringSplitter}
     */
    private static StringSplitter createStringSplitter(String string, char delimiter) {
        return new QuoteAwareStringSplitter(string, delimiter, SplitOption.TRIM_WHITESPACE,
                SplitOption.SPLIT_ON_NEWLINE);
    }

}