blob: cef8aebb05f1ac50aaaa9b430cb581e3f4d474d8 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.nlpcraft.model.tools.sqlgen.impl
import java.io.{File, FileOutputStream, IOException, PrintStream}
import java.sql.{Connection, DriverManager, ResultSet}
import java.time.format.DateTimeFormatter
import java.time.{Instant, LocalDateTime}
import java.util
import java.util.regex.{Pattern, PatternSyntaxException}
import com.fasterxml.jackson.annotation.JsonInclude.Include
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.Feature
import org.apache.nlpcraft.common.ascii.NCAsciiTable
import org.apache.nlpcraft.common.nlp.core.NCNlpPorterStemmer
import org.apache.nlpcraft.common.version.NCVersion
import org.apache.nlpcraft.model.impl.json.{NCElementJson, NCMacroJson, NCModelJson}
import org.apache.nlpcraft.model.tools.sqlgen.NCSqlJoinType
import org.apache.nlpcraft.common._
import resource.managed
import scala.collection.JavaConverters._
import scala.collection.immutable.HashMap
import scala.collection.{Seq, mutable}
import scala.util.Try
/**
* Scala-based SQL model engine.
*/
object NCSqlModelGeneratorImpl {
private var repl = false
case class Join(
fromColumns: Seq[String],
toTable: String,
toColumns: Seq[String]
)
trait NamedEntity {
val nameLc: String
val elmNameLc: String
private val nameWs = elmNameLc.replaceAll("_", " ").split(" ").filter(_.nonEmpty).mkString(" ")
lazy val synonym =
if (elmNameLc == nameWs)
substituteMacros(elmNameLc)
else
s"{${substituteMacros(elmNameLc)}|${substituteMacros(removeSeqDups(nameWs))}}"
}
// https://docs.oracle.com/javase/8/docs/api/java/sql/DatabaseMetaData.html#getColumns-java.lang.String-java.lang.String-java.lang.String-java.lang.String-
case class Column(
name: String,
elmName: String, // Name with optionally removed prefix and suffix.
dataType: Int,
isNullable: String,
isPk: Boolean
) extends NamedEntity {
val nameLc = name.toLowerCase()
val elmNameLc = elmName.toLowerCase()
lazy val isNull = isNullable == "YES"
}
case class Table(
name: String,
elmName: String, // Name with optionally removed prefix and suffix.
joins: Seq[Join],
columns: mutable.ArrayBuffer[Column] = mutable.ArrayBuffer.empty[Column]
) extends NamedEntity {
val nameLc = name.toLowerCase()
val elmNameLc = elmName.toLowerCase()
}
case class ParametersHolder(
var cmdLine: String = null,
var url: String = null,
var driver: String = null,
var user: String = null,
var password: String = null,
var output: String = null,
var schema: String = null,
var modelId: String = "sql.model.id",
var modelVer: String = s"1.0.0-${Instant.now.getEpochSecond}",
var modelName: String = "SQL-based model.",
var synonyms: Boolean = true,
var parent: Boolean = false,
var preSpec: String = "",
var sufSpec: String = "",
var inclSpec: String = "",
var exclSpec: String = "",
var inclPred: (String, String) ⇒ Boolean = (_, _) ⇒ true,
var exclPred: (String, String) ⇒ Boolean = (_, _) ⇒ false,
var preFun: String ⇒ String = s ⇒ s,
var sufFun: String ⇒ String = s ⇒ s,
var overRide: Boolean = false // 'override' is a reserved keyword.
) {
lazy val isJson: Boolean = {
require(output != null)
val lc = output.toLowerCase()
lc.endsWith(".js") || lc.endsWith(".json")
}
}
final private val ver = NCVersion.getCurrent
/**
*
* @param s
* @return
*/
def substituteMacros(s: String): String =
s.split(" ").filter(_.nonEmpty).map(w ⇒ {
if (w == "id")
"<ID>"
else
w
}).mkString(" ")
/**
* Note that it only removed one, first found, prefix.
*
* @param s
* @return
*/
private def mkPrefixFun(s: String): String ⇒ String = {
val arr = s.split(",").map(_.trim).filter(_.nonEmpty)
z ⇒ (for (fix ← arr if z.startsWith(fix)) yield z.substring(fix.length)).headOption.getOrElse(z)
}
/**
* Note that it only removed one, first found, prefix.
*
* @param s
* @return
*/
private def mkSuffixFun(s: String): String ⇒ String = {
val arr = s.split(",").map(_.trim).filter(_.nonEmpty)
z ⇒ (for (fix ← arr if z.endsWith(fix)) yield z.substring(0, z.length - fix.length)).headOption.getOrElse(z)
}
/**
*
* @param s Semicolon-separate list of include/exclude expressions.
* @return
*/
private def mkPredicate(s: String): (String, String) ⇒ Boolean = {
def convert(expr: String): (String, String) ⇒ Boolean = {
val s = expr.split("#").filter(!_.isEmpty)
val (tbl: String, col: String) = s.length match {
case 1 if !expr.contains("#") ⇒ (s(0), "") // 'table'
case 1 if expr.contains("#") ⇒ ("", s(0)) // '#column'
case 2 ⇒ (s(0), s(1)) // 'table#column'
case _ ⇒ throw new Exception(s"Invalid table and/or column filter: $C$expr$RST")
}
val (tblRx, colRx) = try {
(
if (tbl != "") Pattern.compile(tbl) else null,
if (col != "") Pattern.compile(col) else null
)
}
catch {
case e: PatternSyntaxException ⇒ throw new Exception(s"Invalid regular expression: $C${e.getMessage}$RST")
}
(t: String, c: String) ⇒ {
require(t != null)
def check(s: String, rx: Pattern): Boolean = {
if (rx == null)
true
else if (s == null)
false
else
rx.matcher(s).matches()
}
check(t, tblRx) && check(c, colRx)
}
}
val predicates = s.split(";").map(_.trim()).map(convert)
(tbl: String, col: String) ⇒ predicates.exists(_(tbl, col))
}
/**
*
* @param m
* @param key
* @param v
*/
private def add(m: java.util.Map[String, AnyRef], key: String, v: Any): Unit = {
val obj = v.asInstanceOf[AnyRef]
if (obj != null) {
val ok = obj match {
case x: String ⇒ x.nonEmpty
case _ ⇒ true
}
if (ok)
m.put(key, obj)
}
}
/**
* Removes sequential duplicate words from given string.
*
* @param syn Synonym to de-dup.
* @return
*/
private def removeSeqDups(syn: String): String = {
val words = syn.split(" ").filter(_.nonEmpty)
words
.zip(words.map(NCNlpPorterStemmer.stem))
.foldRight[List[(String, String)]](Nil)((pair, list) ⇒ list.headOption match {
case Some(head) if head._2 == pair._2 ⇒ list // Skip duplicate 'w' stems.
case _ ⇒ pair :: list
}).map(_._1).mkString(" ")
}
/**
*
* @param tables
* @param params
*/
private def generateModel(tables: Seq[Table], params: ParametersHolder): Unit = {
val elems = mutable.ArrayBuffer.empty[NCElementJson]
for (tbl ← tables) {
val e = new NCElementJson
e.setId(s"tbl:${tbl.nameLc}")
e.setDescription(s"Auto-generated from '${tbl.nameLc}' table.")
e.setGroups(Array("table"))
val meta = new util.LinkedHashMap[String, Object]()
add(meta, "sql:name", tbl.nameLc)
add(meta, "sql:defaultselect", tbl.columns.take(3).map(_.nameLc).asJava) // First 3 columns by default.
add(meta, "sql:defaultsort", tbl.columns.filter(_.isPk).map(col ⇒ s"${tbl.nameLc}.${col.nameLc}#desc").asJava)
add(meta, "sql:extratables", tbl.joins.map(_.toTable.toLowerCase).toSet.asJava)
e.setMetadata(meta)
if (params.synonyms)
e.setSynonyms(Array(tbl.synonym).distinct)
elems += e
}
val tablesMap = tables.map(t ⇒ t.name → t).toMap
for (tbl ← tables; col ← tbl.columns) {
val e = new NCElementJson
e.setId(s"col:${tbl.nameLc}_${col.nameLc}")
e.setDescription(s"Auto-generated from '${tbl.nameLc}.${col.nameLc}' column.")
e.setGroups(Array("column"))
if (params.parent)
e.setParentId(tbl.nameLc)
val meta = new util.LinkedHashMap[String, Object]()
add(meta, "sql:name", col.nameLc)
add(meta, "sql:tablename", tbl.nameLc)
add(meta, "sql:datatype", col.dataType)
add(meta, "sql:isnullable", col.isNull)
add(meta, "sql:ispk", col.isPk)
e.setMetadata(meta)
if (params.synonyms) {
e.setSynonyms(Array(
col.synonym,
s"${tbl.synonym} ${col.synonym}",
s"${col.synonym} <OF> ${tbl.synonym}"
).distinct)
}
elems += e
}
val mdl = new NCModelJson()
mdl.setId(params.modelId)
mdl.setName(params.modelName)
mdl.setVersion(params.modelVer)
mdl.setDescription(s"SQL-based model auto-generated by NCSqlModelGenerator utility.")
mdl.setMacros(Array(
new NCMacroJson("<OF>", "{of|for|per}"),
new NCMacroJson("<ID>", "{unique|*} {id|identifier}")
))
mdl.setAdditionalStopwords(Array())
mdl.setSuspiciousWords(Array())
mdl.setEnabledBuiltInTokens(Array())
mdl.setIntents(Array())
mdl.setMetadata(HashMap[String, Object](
"sql:timestamp" → s"${Instant.now}",
"sql:url" → params.url,
"sql:driver" → params.driver,
"sql:user" → params.user,
"sql:output" → params.output,
"sql:schema" → params.schema,
"sql:cmdline" → params.cmdLine,
"sql:joins" →
tables.flatMap(t ⇒ t.joins.map(j ⇒ {
val fromTable = t.name.toLowerCase
val toTable = j.toTable.toLowerCase
val fromCols = j.fromColumns.map(_.toLowerCase)
val toCols = j.toColumns.map(_.toLowerCase)
def mkNullables(t: String, cols: Seq[String]): Seq[Boolean] = {
val tabCols = tablesMap(t).columns
cols.map(col ⇒ tabCols.find(_.name == col).get.isNull)
}
val fromColsNulls = mkNullables(fromTable, fromCols)
val toColsNulls =mkNullables(toTable, toCols)
def forall(seq: Seq[Boolean], v: Boolean): Boolean = seq.forall(_ == v)
val typ =
if (forall(fromColsNulls, v = true) && forall(toColsNulls, v = false))
NCSqlJoinType.LEFT
else if (forall(fromColsNulls, v = false) && forall(toColsNulls, v = true))
NCSqlJoinType.RIGHT
else
// Default value.
NCSqlJoinType.INNER
val m = new util.LinkedHashMap[String, Object]()
m.put("fromtable", fromTable)
m.put("fromcolumns", fromCols.asJava)
m.put("totable", toTable)
m.put("tocolumns", toCols.asJava)
m.put("jointype", typ.toString.toLowerCase)
m
})).asJava
).asJava)
mdl.setElements(elems.toArray)
val mapper =
if (params.isJson)
new ObjectMapper()
else
new ObjectMapper(new YAMLFactory().disable(Feature.WRITE_DOC_START_MARKER))
mapper.setSerializationInclusion(Include.NON_NULL)
mapper.setSerializationInclusion(Include.NON_EMPTY)
var file = new File(params.output)
if (file.exists()) {
if (!params.overRide) {
val fileName = file.getName
val name = fileName.takeWhile(_ != '.')
val ext = fileName.drop(name.length + 1)
val unique = DateTimeFormatter.ofPattern("MMddA").format(LocalDateTime.now())
file = new File(file.getParent, s"${name}_$unique.$ext")
println(
s"Output file already exist and override is disabled:\n" +
s" +-→ using $C'${file.getName}'$RST filename instead."
)
}
else
println(
s"Existing file '${file.getName}' will be overridden:\n" +
s" +-→ use $C'-z false'$RST to disable override."
)
}
try {
managed(new FileOutputStream(file)) acquireAndGet { stream ⇒
mapper.writerWithDefaultPrettyPrinter().writeValue(stream, mdl)
stream.flush()
}
}
catch {
case e: IOException ⇒ errorExit(s"Failed to write output file '${file.getAbsolutePath}': ${e.getMessage}")
}
val tbl = NCAsciiTable()
tbl += ("JDBC URL", params.url)
tbl += ("JDBC driver", params.driver)
tbl += ("SQL schema", params.schema)
tbl += ("SQL user", if (params.user == null) "" else params.user)
tbl += ("SQL user password", if (params.password == null) "" else params.password.replaceAll(".", "*"))
tbl += ("Generate synonyms", params.synonyms)
tbl += ("Override output", params.overRide)
tbl += ("Use parent ID", params.parent)
tbl += ("Includes", params.inclSpec)
tbl += ("Excludes", params.exclSpec)
tbl += ("Prefix to remove", params.preSpec)
tbl += ("Suffix to remove", params.sufSpec)
tbl += ("Model ID", params.modelId)
tbl += ("Model name", params.modelName)
tbl += ("Model version", params.modelVer)
tbl += ("Tables scanned", tables.size)
tbl += ("Columns scanned", tables.flatMap(_.columns).size)
tbl += ("Model elements", elems.size)
println(s"Model generated: ${file.getAbsolutePath}")
println(tbl)
if (params.synonyms)
println("NOTE: review auto-generated synonyms for validness.")
}
/**
* @param rs
* @param col
* @return
*/
private def getString(rs: ResultSet, col: String): String =
Try(rs.getString(col)).getOrElse("No data")
/**
* @param rs
* @param col
* @return
*/
private def getInt(rs: ResultSet, col: String): Int =
Try(rs.getInt(col)).getOrElse(-1)
/**
*
* @param params
* @return
*/
private def readSqlMetadata(params: ParametersHolder): Seq[Table] = {
val tables = mutable.HashMap.empty[String, Table]
try {
Class.forName(params.driver)
def getConnection: Connection =
if (params.user == null)
DriverManager.getConnection(params.url)
else
DriverManager.getConnection(params.url, params.user, params.password)
val pks = mutable.HashSet.empty[String]
managed { getConnection } acquireAndGet { conn ⇒
val md = conn.getMetaData
managed {
md.getColumns(null, params.schema, null, null)
} acquireAndGet { rs ⇒
while (rs.next()) {
val schNameOrigin = rs.getString("TABLE_SCHEM")
val tblNameOrigin = rs.getString("TABLE_NAME")
val colNameOrigin = rs.getString("COLUMN_NAME")
val schName = schNameOrigin.toLowerCase
val tblName = tblNameOrigin.toLowerCase
val colName = colNameOrigin.toLowerCase
val key = s"$schName.$tblName"
/**
*
* @param tbl
* @param col
* @return
*/
def isAllowed(tbl: String, col: String = null): Boolean =
params.inclPred.apply(tbl, col) && !params.exclPred.apply(tbl, col)
val tbl: Table = tables.get(key) match {
case Some(t) ⇒ t
case None if isAllowed(tblName) ⇒
pks.clear()
managed { md.getPrimaryKeys(null, schNameOrigin, tblNameOrigin) } acquireAndGet { rs ⇒
while (rs.next())
pks += rs.getString("COLUMN_NAME").toLowerCase
}
case class Fk(
name: String,
fromTableSchema: String,
fromTable: String,
fromColumn: String,
toTableSchema: String,
toTable: String,
toColumn: String
)
val fks = mutable.ArrayBuffer.empty[Fk]
managed { md.getImportedKeys(null, schNameOrigin, tblNameOrigin) } acquireAndGet { rs ⇒
while (rs.next())
fks += Fk(
name = rs.getString("FK_NAME"),
fromTableSchema = rs.getString("FKTABLE_SCHEM"),
fromTable = rs.getString("FKTABLE_NAME"),
fromColumn = rs.getString("FKCOLUMN_NAME"),
toTableSchema = rs.getString("PKTABLE_SCHEM"),
toTable = rs.getString("PKTABLE_NAME"),
toColumn = rs.getString("PKCOLUMN_NAME")
)
}
val joins =
fks.filter(fk ⇒
fk.fromTableSchema == null && fk.toTableSchema == null ||
fk.fromTableSchema == schNameOrigin && fk.toTableSchema == schNameOrigin
).groupBy(_.name).map { case (_, fksGrp) ⇒
Join(
fromColumns = fksGrp.map(_.fromColumn),
toTable = fksGrp.head.toTable,
toColumns = fksGrp.map(_.toColumn)
)
}
val t = Table(
tblName,
params.preFun(params.sufFun(tblName)),
joins.toSeq
)
tables += key → t
t
case _ ⇒ null
}
if (tbl != null && isAllowed(tblName, colName))
tbl.columns +=
Column(
name = colName,
params.preFun(params.sufFun(colName)),
dataType = getInt(rs, "DATA_TYPE"),
isNullable = getString(rs, "IS_NULLABLE"),
isPk = pks.contains(colName)
)
}
}
}
}
catch {
case _: ClassNotFoundException ⇒ errorExit(s"Unknown JDBC driver class: $C${params.driver}$RST")
case e: Exception ⇒ errorExit(s"Failed to generate model for $C'${params.url}'$RST: ${e.getLocalizedMessage}")
}
tables.values.toSeq.filter(_.columns.nonEmpty)
}
/**
*
*/
private def help(out: PrintStream): Unit =
out.println(
s"""
|${bo("NAME:")}
| ${c("NCSqlModelGenerator")} -- NLPCraft model generator from SQL databases.
|
|${bo("SYNOPSIS:")}
| java -cp apache-nlpcraft-incubating-${ver.version}-all-deps.jar org.apache.nlpcraft.model.tools.sqlgen.NCSqlModelGenerator [PARAMETERS]
|
|${bo("DESCRIPTION:")}
| This utility generates NLPCraft model stub from a given SQL database schema. You
| can choose database schema, set of tables and columns for which you
| want to generate NLPCraft model. After the model is generated you can
| further configure and customize it for your specific needs.
|
| This Java class can be run from the command line or from an IDE like any other
| Java application. Note that required JDBC driver class must be available on the
| classpath and therefore its JAR should be added to the classpath when running
| this application.
|
|${bo("PARAMETERS:")}
| ${c("--url|-r")} ${g("url")}
| Mandatory database JDBC URL.
|
| ${c("--driver|-d")} ${g("class")}
| Mandatory JDBC driver class. Note that 'class' must be a
| fully qualified class name. It should also be available on
| the classpath.
|
| ${c("--schema|-s")} ${g("schema")}
| Mandatory database schema to scan.
|
| ${c("--out|-o")} ${g("filename")}
| Mandatory name of the output JSON or YAML model file. It should
| have one of the following extensions: .js, .json, .yml, or .yaml
| File extension determines the output file format.
|
| ${c("--user|-u")} ${g("username")}
| Optional database user name.
|
| ${c("--password|-w")} ${g("password")}
| Optional database user password.
|
| ${c("--model-id|-x")} ${g("id")}
| Optional generated model ID. By default, the model ID will be 'sql.model.id'.
|
| ${c("--model-ver|-v")} ${g("version")}
| Optional generated model version. By default, the model version will be '1.0.0-timestamp'.
|
| ${c("--model-name|-n")} ${g("name")}
| Optional generated model name. By default, the model name will be 'SQL-based model'.
|
| ${c("--exclude|-e")} ${g("list")}
| Optional semicolon-separate list of tables and/or columns to exclude. By
| default, none of the tables and columns in the schema are excluded. See below
| for more information.
|
| ${c("--prefix|-f")} ${g("list")}
| Optional comma-separate list of table or column name prefixes to remove.
| These prefixes will be removed when name is used for model elements
| synonyms. By default, no prefixes will be removed.
|
| ${c("--suffix|-q")} ${g("list")}
| Optional comma-separate list of table or column name suffixes to remove.
| These suffixes will be removed when name is used for model elements
| synonyms. By default, no suffixes will be removed.
|
| ${c("--include|-i")} ${g("list")}
| Optional semicolon-separate list of tables and/or columns to include. By
| default, all tables and columns in the schema are included. See below
| for more information.
|
| ${c("--synonyms|-y")} ${g("true|false")}
| Optional flag on whether or not to generated auto synonyms for the model elements.
| Default is true.
|
| ${c("--override|-z")} ${g("true|false")}
| Optional flag to determine whether or not to override output file if it already exist.
| If override is disabled (default) and output file exists - a unique file name will
| be used instead.
| Default is false.
|
| ${c("--parent|-p")} ${g("true|false")}
| Optional flag on whether or not to use element's parent relationship for
| defining SQL columns and their containing (i.e. parent) tables.
| Default is false.
|
| ${c("--help|-h|-?")}
| Prints this usage information.
|
|${bo("DETAILS:")}
| ${c("-r")}, ${c("-d")}, ${c("-s")}, and ${c("-o")} are mandatory parameters, everything else is optional.
|
| Each -i or -e parameter is a semicolon ';' separated list of table or columns names.
| Each table or column name can be one of following forms:
| - ${g("table")} -- to filter on table names only.
| - ${g("table#column")} -- to filter on both table and column names.
| - ${g("#column")} -- to filter on columns only (regardless of the table).
|
| Table and column names are treated as standard Java regular expressions. Note that
| both '#' and ';' cannot be used inside of the regular expression:
|
| ${c("-e")} "${g("#_.+")}" -- excludes any columns starting with '_'.
| ${c("-e")} "${g("tmp.+")}" -- excludes all tables starting with 'tmp'.
| ${c("-i")} "${g("Order.*;#[^_].+")}" -- includes only tables starting with 'Order' and columns that
| do not start with '_'.
|
|${bo("EXAMPLES:")}
| java -cp apache-nlpcraft-incubating-${ver.version}-all-deps.jar org.apache.nlpcraft.model.tools.sqlgen.NCSqlModelGenerator
| ${c("-r")} jdbc:postgresql://localhost:5432/mydb
| ${c("-d")} org.postgresql.Driver
| ${c("-f")} "tbl_, col_"
| ${c("-q")} "_tmp, _old, _unused"
| ${c("-s")} public
| ${c("-e")} "#_.+"
| ${c("-o")} model.json
""".stripMargin
)
/**
*
* @param msg Optional error message.
*/
private def errorExit(msg: String = null): Unit = {
if (repl)
throw new Exception(msg)
else {
if (msg != null)
System.err.println(
s"""
|${r("ERROR:")}
| $msg""".stripMargin
)
help(System.err)
throw new Exception(msg)
}
}
/**
*
* @param v
* @param name
*/
private def mandatoryParam(v: String, name: String): Unit =
if (v == null)
throw new IllegalArgumentException(s"Missing mandatory parameter: $C$name$RST")
/**
*
* @param v
* @param name
* @return
*/
private def parseBoolean(v: String, name: String): Boolean =
v.toLowerCase match {
case "true" ⇒ true
case "false" ⇒ false
case _ ⇒ throw new IllegalArgumentException(s"Invalid boolean value: $C$name $v$RST")
}
/**
*
* @param cmdArgs
* @return
*/
private def parseCmdParameters(cmdArgs: Array[String]): ParametersHolder = {
val params = ParametersHolder()
var i = 0
try {
while (i < cmdArgs.length - 1) {
val k = cmdArgs(i).toLowerCase
val v = cmdArgs(i + 1)
k match {
case "--url" | "-r" ⇒ params.url = v
case "--driver" | "-d" ⇒ params.driver = v
case "--user" | "-u" ⇒ params.user = v
case "--password" | "-w" ⇒ params.password = v
case "--schema" | "-s" ⇒ params.schema = v
case "--model-id" | "-x" ⇒ params.modelId = v
case "--model-name" | "-n" ⇒ params.modelName = v
case "--model-ver" | "-v" ⇒ params.modelVer = v
case "--out" | "-o" ⇒ params.output = v
case "--include" | "-i" ⇒ params.inclSpec = v; params.inclPred = mkPredicate(v)
case "--exclude" | "-e" ⇒ params.exclSpec = v; params.exclPred = mkPredicate(v)
case "--prefix" | "-f" ⇒ params.preSpec = v; params.preFun = mkPrefixFun(v)
case "--suffix" | "-q" ⇒ params.sufSpec = v; params.sufFun = mkSuffixFun(v)
case "--parent" | "-p" ⇒ params.parent = parseBoolean(v, k)
case "--synonyms" | "-y" ⇒ params.synonyms = parseBoolean(v, k)
case "--override" | "-z" ⇒ params.overRide = parseBoolean(v, k)
case _ ⇒ throw new IllegalArgumentException(s"Invalid argument: $C${cmdArgs(i)}$RST")
}
i = i + 2
}
mandatoryParam(params.url, "--url")
mandatoryParam(params.driver, "--driver")
mandatoryParam(params.output, "--out")
mandatoryParam(params.schema, "--schema")
val outLc = params.output.toLowerCase
if (!outLc.endsWith(".json") &&
!outLc.endsWith(".js") &&
!outLc.endsWith(".yaml") &&
!outLc.endsWith(".yml"))
throw new IllegalArgumentException(s"Unsupported output file extension in $C'${params.output}'$RST output.")
params.cmdLine = cmdArgs.mkString(" ")
}
catch {
case e: Exception ⇒ errorExit(e.getMessage)
}
params
}
/**
*
* @param repl Whether or not this is called from REPL.
* @param args Command line arguments.
*/
def process(repl: Boolean, args: Array[String]): Unit = {
this.repl = repl
if (args.isEmpty || !args.intersect(Seq("--help", "-h", "-help", "--?", "-?", "/?", "/help")).isEmpty)
help(System.out)
else {
val params = parseCmdParameters(args)
val tbls = readSqlMetadata(params)
generateModel(tbls, params)
}
}
}