blob: f7c6ef7aa8c0a5e42fde9f15d58af085c7cb1ada [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
*
* https://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.common.config
import com.typesafe.config.{Config, ConfigFactory}
import com.typesafe.scalalogging.LazyLogging
import org.apache.nlpcraft.common._
import java.util.{List => JList}
import java.io.File
import java.net.{MalformedURLException, URL}
import scala.jdk.CollectionConverters.MapHasAsScala
/**
* Mixin for configuration factory based on https://github.com/lightbend/config.
*/
trait NCConfigurable {
import NCConfigurable._
// Accessor to the loaded config. It should reload config.
private def hocon: Config = cfg
/**
*
* @param name Full configuration property path (name).
*/
private def checkMandatory(name: String): Unit =
if (!hocon.hasPath(name))
throw new NCE(s"Mandatory configuration property not found: $name")
/**
* Gets mandatory configuration property.
*
* @param name Full configuration property path (name).
*/
def getInt(name: String): Int = {
checkMandatory(name)
hocon.getInt(name)
}
/**
* Gets mandatory configuration property.
*
* @param name Full configuration property path (name).
*/
def getBool(name: String): Boolean = {
checkMandatory(name)
hocon.getBoolean(name)
}
/**
* Gets optional configuration property.
*
* @param name Full configuration property path (name).
*/
def getBoolOpt(name: String): Option[Boolean] =
if (!hocon.hasPath(name)) None else Some(hocon.getBoolean(name))
/**
* Gets mandatory configuration property.
*
* @param name Full configuration property path (name).
*/
def getLong(name: String): Long = {
checkMandatory(name)
hocon.getLong(name)
}
/**
* Gets mandatory configuration property.
*
* @param name Full configuration property path (name).
*/
def getConfig(name: String): Config = {
checkMandatory(name)
hocon.getConfig(name)
}
/**
* Gets optional configuration property.
*
* @param name Full configuration property path (name).
*/
def getConfigOpt(name: String): Option[Config] =
if (!hocon.hasPath(name)) None else Some(hocon.getConfig(name))
/**
* Gets mandatory configuration property.
*
* @param name Full configuration property path (name).
*/
def getLongList(name: String): JList[java.lang.Long] = {
checkMandatory(name)
hocon.getLongList(name)
}
/**
* Gets mandatory configuration property in `host:port` format.
*
* @param name Full configuration property path (name).
*/
def getHostPort(name: String): (String, Integer) = {
checkMandatory(name)
val ep = getString(name)
val i = ep.indexOf(':')
if (i <= 0)
throw new NCE(s"Invalid 'host:port' endpoint configuration property format [" +
s"name=$name, " +
s"endpoint=$ep" +
s"]")
try
ep.substring(0, i) -> ep.substring(i + 1).toInt
catch {
case _: NumberFormatException =>
throw new NCE(s"Invalid 'host:port' endpoint configuration property port [" +
s"name=$name, " +
s"endpoint=$ep" +
s"]")
}
}
/**
* Gets optional configuration property in `host:port` format.
*
* @param name Full configuration property path (name).
*/
def getHostPortOpt(name: String): Option[(String, Integer)] =
if (hocon.hasPath(name)) Some(getHostPort(name)) else None
/**
* Gets optional configuration property in `host:port` format.
*
* @param name Full configuration property path (name).
* @param dfltHost Default host value.
* @param dfltPort Default port value.
*/
def getHostPortOrElse(name: String, dfltHost: String, dfltPort: Int): (String, Integer) =
getHostPortOpt(name).getOrElse(dfltHost -> dfltPort)
/**
* Gets mandatory configuration property.
*
* @param name Full configuration property path (name).
*/
def getString(name: String): String = {
checkMandatory(name)
hocon.getString(name)
}
/**
* Gets mandatory configuration property.
*
* @param name Full configuration property path (name).
*/
def getObject[T](name: String, f: String => T): T = {
checkMandatory(name)
val v = hocon.getString(name)
try
f(v)
catch {
case _: Exception =>
throw new NCE(s"Configuration property cannot be extracted [" +
s"name=$name, " +
s"value='$v'" +
s"]")
}
}
/**
* Gets mandatory configuration property.
*
* @param name Full configuration property path (name).
*/
def getMap[K, V](name: String): Map[K, V] = {
checkMandatory(name)
try
hocon.getAnyRef(name).asInstanceOf[java.util.Map[K, V]].asScala.toMap
catch {
case e: ClassCastException =>
throw new NCE(s"Configuration property has unexpected type (expecting 'java.util.Map') [" +
s"name=$name" +
s"]", e)
}
}
/**
* Gets optional configuration property.
*
* @param name Full configuration property path (name).
*/
def getMapOpt[K, V](name: String): Option[Map[K, V]] =
if (!hocon.hasPath(name)) None else Some(getMap(name))
/**
* Gets optional configuration property.
*
* @param name Full configuration property path (name).
*/
def getStringOpt(name: String): Option[String] =
if (!hocon.hasPath(name)) None else Some(hocon.getString(name))
/**
* Gets optional configuration property.
*
* @param name Full configuration property path (name).
*/
def getStringOrElse(name: String, dflt: String): String =
getStringOpt(name).getOrElse(dflt)
/**
* Gets mandatory configuration property.
* Note that processed parameter is CSV single string.
*
* @param name Full configuration property path (name).
*/
def getStringList(name: String): Seq[String] = {
checkMandatory(name)
parseCsv(hocon.getString(name))
}
/**
* Gets optional configuration property.
* Note that processed parameter is CSV single string.
*
* @param name Full configuration property path (name).
*/
def getStringListOpt(name: String): Option[Seq[String]] = if (!hocon.hasPath(name)) None else Some(getStringList(name))
/**
*
* @param s
*/
private def parseCsv(s: String): Seq[String] = U.splitTrimFilter(s,",")
}
object NCConfigurable extends LazyLogging {
private var cfg: Config = _
/**
* Initializes system-wide configuration singleton with given parameters. All specific implementations
* of `NCConfigurable` trait will reuse this config instance.
* <p>
* Override configuration, if given, will override any other loaded or merged configuration.
* If configuration file name is provided it will be looked up in the current working directory or on the
* classpath (as class loader resource). Default configuration, if provided, will be used as a final
* fallback.
* <p>
* To override configuration from outside of JVM using environment variables:
* 1. Set system variable -Dconfig.override_with_env_vars=true
* 2. Use environment variables in a form of 'CONFIG_FORCE_x_y_z' to override configuration
* property 'x.y.z' from the file.
* <p>
* Examples:
* CONFIG_FORCE_nlpcraft_server_rest_host="localhost"
* CONFIG_FORCE_nlpcraft_server_models="com.mymodels.MyModel"
*
* @param cfgFile File name.
* @param overrideCfg Optional overriding configuration.
* @param dfltCfg Optional default config.
* @param valFun Validation method.
*/
def initialize(
cfgFile: String,
overrideCfg: Option[Config],
dfltCfg: Option[Config],
valFun: Config => Boolean
): Unit = {
val tmpCfg = {
logger.info(s"Attempting to load/merge configuration from configuration file: $cfgFile")
// Order is: file, URL, resource (File and URL can override resource)
var cfg = ConfigFactory.parseFile(new File(cfgFile))
try
cfg = cfg.withFallback(ConfigFactory.parseURL(new URL(cfgFile)))
catch {
case _: MalformedURLException => // No-op.
}
cfg = cfg.withFallback(ConfigFactory.parseResources(cfgFile))
if (dfltCfg.isDefined)
cfg = cfg.withFallback(dfltCfg.get)
cfg
}
// Validate.
if (!valFun(tmpCfg)) {
logger.error(s"Invalid configuration.")
logger.error(s"Note that you can use environment variable to provide configuration properties - see https://nlpcraft.apache.org/server-and-probe.html.")
throw new NCE(s"No valid configuration found: ${tmpCfg.origin().description()}")
}
else {
if (overrideCfg.isDefined)
cfg = ConfigFactory.load(overrideCfg.get).withFallback(tmpCfg)
else
cfg = ConfigFactory.load(tmpCfg)
val lines = U.splitTrimFilter(cfg.origin().description(),",").drop(1).distinct
logger.info(s"NLPCraft configuration successfully loaded as a merge of: ${lines.mkString(s"\n ${c("+--")} ", s"\n ${c("+--")} ", "")}")
}
// Set parsed configuration into Java shim.
NCConfigurableJava.setConfig(cfg)
}
}