blob: 4a66867903e7661048fb65e80e408d0f30c1e12a [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.openwhisk.core.entity
import scala.util.{Failure, Success, Try}
import spray.json.DefaultJsonProtocol._
import spray.json._
import scala.language.postfixOps
import org.apache.openwhisk.core.entity.size.SizeInt
import org.apache.openwhisk.core.entity.size.SizeString
/**
* Parameters is a key-value map from parameter names to parameter values. The value of a
* parameter is opaque, it is treated as string regardless of its actual type.
*
* @param key the parameter name, assured to be non-null because it is a value
* @param value the parameter value, assured to be non-null because it is a value
*/
protected[core] class Parameters protected[entity] (protected[entity] val params: Map[ParameterName, ParameterValue])
extends AnyVal {
/**
* Calculates the size in Bytes of the Parameters-instance.
*
* @return Size of instance as ByteSize
*/
def size = {
params
.map { case (name, value) => name.size + value.size }
.foldLeft(0 B)(_ + _)
}
protected[entity] def +(p: ParameterName, v: ParameterValue) = {
new Parameters(params + (p -> v))
}
/** Add parameters from p to existing map, overwriting existing values in case of overlap in keys. */
protected[core] def ++(p: Parameters) = new Parameters(params ++ p.params)
/** Add optional parameters from p to existing map, overwriting existing values in case of overlap in keys. */
protected[core] def ++(p: Option[Parameters]): Parameters = {
p.map(x => new Parameters(params ++ x.params)).getOrElse(this)
}
/** Remove parameter by name. */
protected[core] def -(p: String): Parameters = {
// wrap with try since parameter name may throw an exception for illegal p
Try(new Parameters(params - new ParameterName(p))) getOrElse this
}
/** Gets set of all defined parameters. */
protected[core] def definedParameters: Set[String] = {
params.keySet filter (params(_).isDefined) map (_.name)
}
/** Gets set of all defined parameters. */
protected[core] def initParameters: Set[String] = {
params.keySet filter (params(_).init) map (_.name)
}
/**
* Gets map of all locked (encrypted) parameters, excluding parameters from given set.
*/
protected[core] def lockedParameters(exclude: Set[String] = Set.empty): Map[String, String] = {
params.collect {
case p if p._2.encryption.isDefined && !exclude.contains(p._1.name) => (p._1.name -> p._2.encryption.get)
}
}
protected[core] def toJsArray = {
JsArray(params map { p =>
val init = if (p._2.init) Some("init" -> JsTrue) else None
val encrypt = p._2.encryption.map(e => ("encryption" -> JsString(e)))
JsObject(Map("key" -> p._1.name.toJson, "value" -> p._2.value) ++ init ++ encrypt)
} toSeq: _*)
}
protected[core] def toJsObject = JsObject(params.map(p => (p._1.name -> p._2.value.toJson)))
override def toString = toJsArray.compactPrint
/**
* Converts parameters to JSON object and merge keys with payload if defined.
* In case of overlap, the keys in the payload supersede.
*/
protected[core] def merge(payload: Option[JsObject]): Some[JsObject] = {
val args = payload getOrElse JsObject.empty
Some { (toJsObject.fields ++ args.fields).toJson.asJsObject }
}
/** Retrieves parameter by name if it exists. */
protected[core] def get(p: String): Option[JsValue] = params.get(new ParameterName(p)).map(_.value)
/** Retrieves parameter by name if it exists. Returns that parameter if it is deserializable to {@code T} */
protected[core] def getAs[T: JsonReader](p: String): Try[T] =
get(p)
.fold[Try[JsValue]](Failure(new IllegalStateException(s"key '$p' does not exist")))(Success.apply)
.flatMap(js => Try(js.convertTo[T]))
/**
* Retrieves parameter by name if it exist.
* @param p the parameter to check for a truthy value
* @param valueForNonExistent the value to return for a missing parameter (default false)
* @return true if parameter exists and has truthy value, otherwise returns the specified value for non-existent keys
*/
protected[core] def isTruthy(p: String, valueForNonExistent: Boolean = false): Boolean = {
get(p) map {
case JsBoolean(b) => b
case JsNumber(n) => n != 0
case JsString(s) => s.nonEmpty
case JsNull => false
case _ => true
} getOrElse valueForNonExistent
}
/**
* Encrypts any parameters that are not yet encoded.
*
* @param encoder the encoder to transform parameter values with
* @return parameters with all values encrypted
*/
def lock(encoder: Option[Encrypter] = None): Parameters = {
encoder
.map { coder =>
new Parameters(params.map {
case (paramName, paramValue) if paramValue.encryption.isEmpty =>
paramName -> coder.encrypt(paramValue)
case p => p
})
}
.getOrElse(this)
}
/**
* Decodes parameters. If the encryption scheme for a parameter is not recognized, it is not modified.
*
* @param decoder the decoder to use to transform locked values
* @return parameters will all values decoded (where scheme is known)
*/
def unlock(decoder: ParameterEncryption): Parameters = {
new Parameters(params.map {
case p @ (paramName, paramValue) =>
paramValue.encryption
.map(paramName -> decoder.encryptor(_).decrypt(paramValue))
.getOrElse(p)
})
}
}
/**
* A ParameterName is a parameter name for an action or trigger to bind to its environment.
* It wraps a normalized string as a value type.
*
* It is a value type (hence == is .equals, immutable and cannot be assigned null).
* The constructor is private so that argument requirements are checked and normalized
* before creating a new instance.
*
* @param name the name of the parameter (its key)
*/
protected[entity] class ParameterName protected[entity] (val name: String) extends AnyVal {
/**
* The size of the ParameterName entity as ByteSize.
*/
def size = name sizeInBytes
}
/**
* A ParameterValue is a parameter value for an action or trigger to bind to its environment.
* It wraps a normalized string as a value type. The string may be a JSON string. It may also
* be undefined, such as when an action is created but the parameter value is not bound yet.
* In general, this is an opaque value.
*
* The constructor is private so that argument requirements are checked and normalized
* before creating a new instance.
*
* @param v the value of the parameter, may be null
* @param init if true, this parameter value is only offered to the action during initialization
* @param encryption the name of the encryption algorithm used to store the parameter or none (plain text)
*/
protected[entity] case class ParameterValue protected[entity] (private val v: JsValue,
val init: Boolean,
val encryption: Option[String] = None) {
/** @return JsValue if defined else JsNull. */
protected[entity] def value = Option(v) getOrElse JsNull
/** @return true iff value is not JsNull. */
protected[entity] def isDefined = value != JsNull
/**
* The size of the ParameterValue entity as ByteSize.
*/
def size = value.toString.sizeInBytes
}
protected[core] object Parameters extends ArgNormalizer[Parameters] {
/** Name of parameter that indicates if action is a feed. */
protected[core] val Feed = "feed"
protected[core] val sizeLimit = 1 MB
protected[core] def apply(): Parameters = new Parameters(Map.empty)
/**
* Creates a parameter tuple from a pair of strings.
* A convenience method for tests.
*
* @param p the parameter name
* @param v the parameter value
* @param init the parameter is for initialization
* @return (ParameterName, ParameterValue)
* @throws IllegalArgumentException if key is not defined
*/
@throws[IllegalArgumentException]
protected[core] def apply(p: String, v: String, init: Boolean = false): Parameters = {
require(p != null && p.trim.nonEmpty, "key undefined")
Parameters() + (new ParameterName(ArgNormalizer.trim(p)),
ParameterValue(Option(v).map(_.trim.toJson).getOrElse(JsNull), init, None))
}
/**
* Creates a parameter tuple from a parameter name and JsValue.
*
* @param p the parameter name
* @param v the parameter value
* @param init the parameter is for initialization
* @return (ParameterName, ParameterValue)
* @throws IllegalArgumentException if key is not defined
*/
@throws[IllegalArgumentException]
protected[core] def apply(p: String, v: JsValue, init: Boolean): Parameters = {
require(p != null && p.trim.nonEmpty, "key undefined")
Parameters() + (new ParameterName(ArgNormalizer.trim(p)),
ParameterValue(Option(v).getOrElse(JsNull), init, None))
}
/**
* Creates a parameter tuple from a parameter name and JsValue.
*
* @param p the parameter name
* @param v the parameter value
* @return (ParameterName, ParameterValue)
* @throws IllegalArgumentException if key is not defined
*/
@throws[IllegalArgumentException]
protected[core] def apply(p: String, v: JsValue): Parameters = {
require(p != null && p.trim.nonEmpty, "key undefined")
Parameters() + (new ParameterName(ArgNormalizer.trim(p)),
ParameterValue(Option(v).getOrElse(JsNull), false, None))
}
override protected[core] implicit val serdes = new RootJsonFormat[Parameters] {
def write(p: Parameters) = p.toJsArray
/**
* Gets parameters as a Parameters instances. The argument should be a JArray
* [{key,value}], otherwise an IllegalParameter is thrown.
*
* @param parameters the JSON representation of an parameter array
* @return Parameters instance if parameters conforms to schema
*/
def read(value: JsValue): Parameters = {
value match {
case JsArray(params) => read(params).getOrElse(deserializationError("parameters malformed!"))
case _ => deserializationError("parameters malformed!")
}
}
/**
* Gets parameters as a Parameters instances.
* The argument should be a [{key,value}].
*
* @param parameters the JSON representation of an parameter array
* @return Parameters instance if parameters conforms to schema
*/
def read(params: Vector[JsValue]) = Try {
new Parameters(params.map {
case o @ JsObject(fields) =>
o.getFields("key", "value", "init", "encryption") match {
case Seq(JsString(k), v: JsValue) if fields.contains("value") =>
val key = new ParameterName(k)
val value = ParameterValue(v, false)
(key, value)
case Seq(JsString(k), v: JsValue, JsBoolean(i)) =>
val key = new ParameterName(k)
val value = ParameterValue(v, i)
(key, value)
case Seq(JsString(k), v: JsValue, JsBoolean(i), JsString(e)) =>
val key = new ParameterName(k)
val value = ParameterValue(v, i, Some(e))
(key, value)
case Seq(JsString(k), v: JsValue, JsBoolean(i), JsNull) =>
val key = new ParameterName(k)
val value = ParameterValue(v, i, None)
(key, value)
case Seq(JsString(k), v: JsValue, JsString(e))
if fields.contains("value") && fields.contains("encryption") =>
val key = new ParameterName(k)
val value = ParameterValue(v, false, Some(e))
(key, value)
}
case _ => deserializationError("invalid parameter")
}.toMap)
}
}
}