package com.anjlab.csv2db;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.Options;
import org.apache.commons.io.input.AutoCloseInputStream;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import au.com.bytecode.opencsv.CSVParser;
import au.com.bytecode.opencsv.CSVReader;
public class Configuration
{
private static final String MODE = "mode";
private static final String DRIVER_CLASS = "driverClass";
private static final String CONNECTION_URL = "connectionUrl";
private static final String USERNAME = "username";
private static final String PASSWORD = "password";
private static final int DEFAULT_BATCH_SIZE = 100;
private static final Gson gson = createGson();
private static Gson createGson()
{
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(ValueDefinition.class, new ValueDefinitionAdapter());
return gsonBuilder.create();
}
public enum OperationMode
{
INSERT, MERGE, INSERTONLY
}
public static class CSVOptions
{
private char separatorChar = CSVParser.DEFAULT_SEPARATOR;
private char quoteChar = CSVParser.DEFAULT_QUOTE_CHARACTER;
private char escapeChar = CSVParser.DEFAULT_ESCAPE_CHARACTER;
private int skipLines = CSVReader.DEFAULT_SKIP_LINES;
private boolean strictQuotes = CSVParser.DEFAULT_STRICT_QUOTES;
private boolean ignoreLeadingWhiteSpace = CSVParser.DEFAULT_IGNORE_LEADING_WHITESPACE;
public char getSeparatorChar()
{
return separatorChar;
}
public void setSeparatorChar(char separatorChar)
{
this.separatorChar = separatorChar;
}
public char getQuoteChar()
{
return quoteChar;
}
public void setQuoteChar(char quoteChar)
{
this.quoteChar = quoteChar;
}
public char getEscapeChar()
{
return escapeChar;
}
public void setEscapeChar(char escapeChar)
{
this.escapeChar = escapeChar;
}
public int getSkipLines()
{
return skipLines;
}
public void setSkipLines(int skipLines)
{
this.skipLines = skipLines;
}
public boolean isStrictQuotes()
{
return strictQuotes;
}
public void setStrictQuotes(boolean strictQuotes)
{
this.strictQuotes = strictQuotes;
}
public boolean isIgnoreLeadingWhiteSpace()
{
return ignoreLeadingWhiteSpace;
}
public void setIgnoreLeadingWhiteSpace(boolean ignoreLeadingWhiteSpace)
{
this.ignoreLeadingWhiteSpace = ignoreLeadingWhiteSpace;
}
}
private OperationMode operationMode;
private String driverClass;
private String connectionUrl;
private Map<String, ValueDefinition> connectionProperties;
private String targetTable;
private List<String> primaryKeys;
/**
* Map keys are zero-based column indices in CSV file.
* Map values are target table column names.
*/
private Map<Integer, String> columnMappings;
private List<String> transientColumns;
private List<String> syntheticColumns;
private Map<String, ValueDefinition> insertValues;
private Map<String, ValueDefinition> updateValues;
private Map<String, ValueDefinition> transform;
private ValueDefinition map;
private List<String> scripting;
private int batchSize = DEFAULT_BATCH_SIZE;
private CSVOptions csvOptions;
private boolean forceUpdate;
private boolean ignoreNullPK;
private boolean ignoreDuplicatePK;
private transient FileResolver fileResolver;
private transient ScriptEngine scriptEngine;
public static Configuration fromJson(String filename) throws FileNotFoundException
{
FileResolver resolverRelativeToParentFolder =
new SimpleFileResolver(new File(filename).getParentFile());
return fromJson(new AutoCloseInputStream(new FileInputStream(new File(filename))),
resolverRelativeToParentFolder);
}
public static Configuration fromJson(InputStream input, FileResolver fileResolver) throws FileNotFoundException
{
Configuration config = gson.fromJson(
readConfig(input, fileResolver), Configuration.class);
if (config.getCsvOptions() == null)
{
config.setCsvOptions(new CSVOptions());
}
config.setFileResolver(fileResolver);
return config;
}
private static JsonElement readConfig(InputStream input, FileResolver fileResolver) throws FileNotFoundException
{
JsonObject config = (JsonObject) new JsonParser().parse(new InputStreamReader(input));
if (config.has("extend"))
{
String parentFilename = config.get("extend").getAsString();
JsonElement parentConfig = readConfig(
new AutoCloseInputStream(new FileInputStream(fileResolver.getFile(parentFilename))),
fileResolver);
return extend(config, parentConfig);
}
return config;
}
private static JsonElement extend(JsonElement configElement, JsonElement parentConfigElement)
{
if (!configElement.isJsonObject() || !parentConfigElement.isJsonObject())
{
return configElement;
}
final JsonObject config = configElement.getAsJsonObject();
final JsonObject parentConfig = parentConfigElement.getAsJsonObject();
for (Entry<String, JsonElement> entry : parentConfig.entrySet())
{
if (!config.has(entry.getKey()))
{
// Copy entire member from parent
config.add(entry.getKey(), entry.getValue());
}
else
{
// Same property declared, but maybe some children don't have overrides?
extend(config.get(entry.getKey()), entry.getValue());
}
}
return config;
}
public String toJson()
{
return gson.toJson(this);
}
public String getDriverClass()
{
return driverClass;
}
public void setDriverClass(String driverClass)
{
this.driverClass = driverClass;
}
public String getConnectionUrl()
{
return connectionUrl;
}
public void setConnectionUrl(String connectionUrl)
{
this.connectionUrl = connectionUrl;
}
public Map<Integer, String> getColumnMappings()
{
return columnMappings;
}
public void setColumnMappings(Map<Integer, String> columnMappings)
{
this.columnMappings = columnMappings;
}
public List<String> getTransientColumns()
{
return transientColumns;
}
public void setTransientColumns(List<String> transientColumns)
{
this.transientColumns = transientColumns;
}
public List<String> getSyntheticColumns()
{
return syntheticColumns;
}
public void setSyntheticColumns(List<String> syntheticColumns)
{
this.syntheticColumns = syntheticColumns;
}
public FunctionReference getMap()
{
return (FunctionReference) map;
}
public void setMap(FunctionReference map)
{
this.map = map;
}
public CSVOptions getCsvOptions()
{
return csvOptions;
}
public void setCsvOptions(CSVOptions csvOptions)
{
this.csvOptions = csvOptions;
}
public List<String> getPrimaryKeys()
{
return primaryKeys;
}
public void setPrimaryKeys(List<String> primaryKeys)
{
this.primaryKeys = primaryKeys;
}
public Map<String, String> getConnectionProperties() throws ConfigurationException
{
Map<String, String> properties = new HashMap<String, String>();
for (Entry<String, ValueDefinition> entry : connectionProperties.entrySet())
{
try
{
ValueDefinition value = entry.getValue();
if (value.producesSQL())
{
throw new ConfigurationException(
"Connection property '" + entry.getKey() + "' produces SQL which is not supported."
+ "Only primitive types and function references allowed here.");
}
properties.put(
entry.getKey(),
String.valueOf(
value.eval(
entry.getKey(),
new HashMap<String, Object>(),
getScriptEngine())));
}
catch (ScriptException e)
{
throw new RuntimeException("Error evaluating connection properties", e);
}
}
return properties;
}
public void setConnectionProperties(Map<String, ValueDefinition> connectionProperties)
{
this.connectionProperties = connectionProperties;
}
public String getTargetTable()
{
return targetTable;
}
public void setTargetTable(String targetTable)
{
this.targetTable = targetTable;
}
public OperationMode getOperationMode()
{
return operationMode;
}
public void setOperationMode(OperationMode operationMode)
{
this.operationMode = operationMode;
}
public Map<String, ValueDefinition> getInsertValues()
{
return insertValues;
}
public void setInsertValues(Map<String, ValueDefinition> insertValues)
{
this.insertValues = insertValues;
}
public Map<String, ValueDefinition> getUpdateValues()
{
return updateValues;
}
public void setUpdateValues(Map<String, ValueDefinition> updateValues)
{
this.updateValues = updateValues;
}
public Map<String, ValueDefinition> getTransform()
{
return transform;
}
public void setTransform(Map<String, ValueDefinition> transform)
{
this.transform = transform;
}
public List<String> getScripting()
{
return scripting;
}
public void setScripting(List<String> scripting)
{
this.scripting = scripting;
}
public int getBatchSize()
{
return batchSize <= 0 ? 1 : batchSize;
}
public void setBatchSize(int batchSize)
{
this.batchSize = batchSize;
}
public boolean isForceUpdate()
{
return forceUpdate;
}
public void setForceUpdate(boolean forceUpdate)
{
this.forceUpdate = forceUpdate;
}
public boolean isIgnoreNullPK()
{
return ignoreNullPK;
}
public void setIgnoreNullPK(boolean ignoreNullPK)
{
this.ignoreNullPK = ignoreNullPK;
}
public boolean isIgnoreDuplicatePK()
{
return ignoreDuplicatePK;
}
public void setIgnoreDuplicatePK(boolean ignoreDuplicatePK)
{
this.ignoreDuplicatePK = ignoreDuplicatePK;
}
public FileResolver getFileResolver()
{
return fileResolver;
}
public void setFileResolver(FileResolver fileResolver)
{
this.fileResolver = fileResolver;
}
public ScriptEngine getScriptEngine()
{
if (scriptEngine == null)
{
try
{
scriptEngine = newScriptEngine();
}
catch (ScriptException | IOException e)
{
throw new RuntimeException("Error loading scripting engine", e);
}
}
return scriptEngine;
}
private ScriptEngine newScriptEngine() throws FileNotFoundException,
ScriptException, IOException
{
ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
ScriptEngine scriptEngine = scriptEngineManager.getEngineByName("Nashorn");
if (scriptEngine == null)
{
scriptEngine = scriptEngineManager.getEngineByName("JavaScript");
}
if (getScripting() != null)
{
for (String filename : getScripting())
{
FileReader scriptReader = new FileReader(fileResolver.getFile(filename));
try
{
scriptEngine.eval(scriptReader);
}
finally
{
scriptReader.close();
}
}
}
return scriptEngine;
}
public static void addOptions(Options options)
{
options
.addOption("m", MODE, true, "Operation mode (INSERT, MERGE, INSERTONLY)")
.addOption("d", DRIVER_CLASS, true, "JDBC driver class name")
.addOption("l", CONNECTION_URL, true, "JDBC connection URL")
.addOption("u", USERNAME, true, "Connection username")
.addOption("p", PASSWORD, true, "Connection password");
}
public Configuration overrideFrom(CommandLine cmd)
{
if (cmd.hasOption(MODE))
{
setOperationMode(OperationMode.valueOf(cmd.getOptionValue(MODE)));
}
if (cmd.hasOption(DRIVER_CLASS))
{
setDriverClass(cmd.getOptionValue(DRIVER_CLASS));
}
if (cmd.hasOption(CONNECTION_URL))
{
setConnectionUrl(cmd.getOptionValue(CONNECTION_URL));
}
if (cmd.hasOption(USERNAME))
{
connectionProperties.put("user", new StringLiteral(cmd.getOptionValue(USERNAME)));
}
if (cmd.hasOption(PASSWORD))
{
connectionProperties.put("password", new StringLiteral(cmd.getOptionValue(PASSWORD)));
}
if (cmd.hasOption(Import.BATCH_SIZE))
{
setBatchSize(Integer.parseInt(cmd.getOptionValue(Import.BATCH_SIZE, "1")));
}
return this;
}
public String joinPrimaryKeys(Map<String, Object> nameValues)
{
StringBuilder builder = new StringBuilder();
for (String primaryKeyColumnName : getPrimaryKeys())
{
if (builder.length() > 0)
{
builder.append(", ");
}
builder
.append(primaryKeyColumnName)
.append("=")
.append(String.valueOf(nameValues.get(primaryKeyColumnName)));
}
return builder.toString();
}
}