org.nlpcraft.server.json.NCJson.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of nlpcraft Show documentation
Show all versions of nlpcraft Show documentation
An API to convert natural language into actions.
/*
* “Commons Clause” License, https://commonsclause.com/
*
* The Software is provided to you by the Licensor under the License,
* as defined below, subject to the following condition.
*
* Without limiting other conditions in the License, the grant of rights
* under the License will not include, and the License does not grant to
* you, the right to Sell the Software.
*
* For purposes of the foregoing, “Sell” means practicing any or all of
* the rights granted to you under the License to provide to third parties,
* for a fee or other consideration (including without limitation fees for
* hosting or consulting/support services related to the Software), a
* product or service whose value derives, entirely or substantially, from
* the functionality of the Software. Any license notice or attribution
* required by the License must also include this Commons Clause License
* Condition notice.
*
* Software: NLPCraft
* License: Apache 2.0, https://www.apache.org/licenses/LICENSE-2.0
* Licensor: Copyright (C) 2018 DataLingvo, Inc. https://www.datalingvo.com
*
* _ ____ ______ ______
* / | / / /___ / ____/________ _/ __/ /_
* / |/ / / __ \/ / / ___/ __ `/ /_/ __/
* / /| / / /_/ / /___/ / / /_/ / __/ /_
* /_/ |_/_/ .___/\____/_/ \__,_/_/ \__/
* /_/
*/
package org.nlpcraft.server.json
import java.io.{IOException, _}
import java.util.zip._
import com.typesafe.scalalogging.LazyLogging
import net.liftweb.json.{compact ⇒ liftCompact, pretty ⇒ liftPretty, render ⇒ liftRender, _}
import org.nlpcraft.common._
import scala.annotation.tailrec
import scala.language.implicitConversions
import scala.util.matching.Regex
/**
* Project-wide, Lift-based general JSON wrapper.
*/
class NCJson(val json: JValue) {
import NCJson._
require(json != null)
// Delegate to underlying JValue.
override def hashCode(): Int = json.hashCode()
override def equals(obj: scala.Any): Boolean = json.equals(obj)
/**
* Convenient method to get JSON unboxed value with given type and name.
*
* @param fn Field name.
* @tparam T Type of the value.
*/
@throws[NCJ]
def field[T](fn: String): T =
try
json \ fn match {
case JNothing | null ⇒ throw MissingJsonField(fn)
case v: JValue ⇒ v.values.asInstanceOf[T]
}
catch {
case e: MissingJsonField ⇒ throw e // Rethrow.
case e: Throwable ⇒ throw InvalidJsonField(fn, e)
}
/**
* Tests whether given JSON field present or not.
*
* @param fn JSON field name.
*/
def hasField(fn: String): Boolean =
json \ fn match {
case JNothing | null ⇒ false
case _: JValue ⇒ true
}
/**
* Convenient method to get JSON unboxed value with given type and name.
*
* @param fn Field name.
* @tparam T Type of the value.
*/
def fieldOpt[T](fn: String): Option[T] =
try
json \ fn match {
case JNothing ⇒ None
case v: JValue ⇒ Some(v.values.asInstanceOf[T])
}
catch {
case _: Throwable ⇒ None
}
/**
* Renders this JSON with proper new-lines and indentation (suitable for human readability).
*
* @return String presentation of this JSON object.
*/
def pretty: String = liftPretty(liftRender(json))
/**
* Renders this JSON in a compact form (suitable for exchange).
*
* @return String presentation of this JSON object.
*/
def compact: String = liftCompact(liftRender(json))
/**
* Zips this JSON object into array of bytes using GZIP.
*/
def gzip(): Array[Byte] = {
val out = new ByteArrayOutputStream(1024)
try {
val gzip = new GZIPOutputStream(out)
gzip.write(compact.getBytes)
gzip.close()
out.toByteArray
}
// Let IOException to propagate unchecked (since it shouldn't appear here by the spec).
finally {
out.close()
}
}
override def toString: String = compact
}
/**
* Static scope for JSON wrapper.
*/
object NCJson {
private type NCJ = NCJsonException
// Specific control flow exceptions.
case class InvalidJson(js: String) extends NCJ(s"Malformed JSON syntax in: $js") with LazyLogging {
// Log right away.
logger.error(s"Malformed JSON syntax in: $js")
}
case class InvalidJsonField(fn: String, cause: Throwable) extends NCJ(s"Invalid '$fn' JSON field <" +
cause.getMessage + ">", cause) with LazyLogging {
require(cause != null)
// Log right away.
logger.error(s"Invalid '$fn' JSON field <${cause.getMessage}>")
}
case class MissingJsonField(fn: String) extends NCJ(s"Missing mandatory '$fn' JSON field.") with LazyLogging {
// Log right away.
logger.error(s"Missing mandatory '$fn' JSON field.")
}
implicit val formats: DefaultFormats.type = net.liftweb.json.DefaultFormats
// Regex for numbers with positive exponent part with explicit + in notation. Example 2E+5.
// Also, these numbers should be pre-fixed and post-fixed by restricted JSON symbols set.
private val EXP_REGEX = {
val mask = "[-+]?([0-9]+\\.?[0-9]*|\\.[0-9]+)([eE]\\+[0-9]+)"
val pre = Seq(' ', '"', '[', ',', ':')
val post = Seq(' ', '"', ']', ',', '}')
def makeMask(chars: Seq[Char]): String = s"[${chars.map(ch ⇒ s"\\$ch").mkString}]"
new Regex(s"${makeMask(pre)}$mask${makeMask(post)}")
}
/**
* Creates JSON wrapper from given string.
*
* @param js JSON string presentation.
* @return JSON wrapper.
*/
@throws[NCJ]
def apply(js: String): NCJson = {
require(js != null)
JsonParser.parseOpt(processExpNumbers(js)) match {
case Some(a) ⇒ new NCJson(a)
case _ ⇒ throw InvalidJson(js)
}
}
/**
* Creates JSON wrapper from given Lift `JValue` object.
*
* @param json Lift `JValue` AST object.
* @return JSON wrapper.
*/
@throws[NCJ]
def apply(json: JValue): NCJson = {
require(json != null)
new NCJson(json)
}
/**
* Unzips array of bytes into string.
*
* @param arr Array of bytes produced by 'gzip' method.
*/
def unzip2String(arr: Array[Byte]): String = {
val in = new ByteArrayInputStream(arr)
val out = new ByteArrayOutputStream(1024)
val tmpArr = new Array[Byte](512)
try {
val gzip = new GZIPInputStream(in)
var n = gzip.read(tmpArr, 0, tmpArr.length)
while (n > 0) {
out.write(tmpArr, 0, n)
n = gzip.read(tmpArr, 0, tmpArr.length)
}
gzip.close()
// Trim method added to delete last symbol of ZLIB compression
// protocol (NULL - 'no error' flag) http://www.zlib.net/manual.html
out.toString("UTF-8").trim
}
// Let IOException to propagate unchecked (since it shouldn't appear here by the spec).
finally {
out.close()
in.close()
}
}
/**
* Unzips array of bytes into JSON object.
*
* @param arr Array of bytes produced by 'gzip' method.
*/
def unzip2Json(arr: Array[Byte]): NCJson = NCJson(unzip2String(arr))
/**
* Reads file.
*
* @param f File to extract from.
*/
private def readFile(f: File): String = removeComments(U.readFile(f, "UTF8").mkString)
/**
* Reads stream.
*
* @param in Stream to extract from.
*/
private def readStream(in: InputStream): String = removeComments(U.readStream(in, "UTF8").mkString)
/**
* Extracts type `T` from given JSON `file`.
*
* @param f File to extract from.
* @param ignoreCase Whether or not to ignore case.
* @tparam T Type of the object to extract.
*/
@throws[NCE]
def extractFile[T: Manifest](f: java.io.File, ignoreCase: Boolean): T =
try
if (ignoreCase) NCJson(readFile(f).toLowerCase).json.extract[T] else NCJson(readFile(f)).json.extract[T]
catch {
case e: IOException ⇒ throw new NCE(s"Failed to read: ${f.getAbsolutePath}", e)
case e: Throwable ⇒ throw new NCE(s"Failed to parse: ${f.getAbsolutePath}", e)
}
/**
* Removes C-style /* */ multi-line comments from JSON.
*
* @param json JSON text.
*/
private def removeComments(json: String): String = json.replaceAll("""\/\*(\*(?!\/)|[^*])*\*\/""", "")
/**
* Extracts type `T` from given JSON `file`.
*
* @param path File path to extract from.
* @param ignoreCase Whether or not to ignore case.
* @tparam T Type of the object to extract.
*/
@throws[NCE]
def extractPath[T: Manifest](path: String, ignoreCase: Boolean): T = extractFile(new java.io.File(path), ignoreCase)
/**
* Extracts type `T` from given JSON `file`.
*
* @param res Resource to extract from.
* @param ignoreCase Whether or not to ignore case.
* @tparam T Type of the object to extract.
*/
@throws[NCE]
def extractResource[T: Manifest](res: String, ignoreCase: Boolean): T =
try {
val in = U.getStream(res)
if (ignoreCase) NCJson(readStream(in).toLowerCase).json.extract[T] else NCJson(readStream(in)).json.extract[T]
}
catch {
case e: IOException ⇒ throw new NCE(s"Failed to read: $res", e)
case e: Throwable ⇒ throw new NCE(s"Failed to parse: $res", e)
}
// Gets string with removed symbol + from exponent part of numbers.
// It is developed to avoid Lift parsing errors during processing numbers like '2E+2'.
@tailrec
def processExpNumbers(s: String): String =
EXP_REGEX.findFirstMatchIn(s) match {
case Some(m) ⇒ processExpNumbers(m.before + m.group(0).replaceAll("\\+", "") + m.after)
case None ⇒ s
}
// Implicit conversions.
implicit def x(jv: JValue): NCJson = new NCJson(jv)
implicit def x1(js: NCJson): JValue = js.json
implicit def x2(likeJs: NCJsonLike): JValue = likeJs.toJson.json
implicit def x3(likeJs: NCJsonLike): NCJson = likeJs.toJson
implicit def x4(js: NCJson): String = js.compact
implicit def x4(js: NCJsonLike): String = js.toJson.compact
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy