/*
* Copyright (c) 2013-2017 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);
}
}