blob: 4898e723ede402119574a1b5463b436da466b52e [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.database.memory
import spray.json.{JsArray, JsBoolean, JsNumber, JsObject, JsString, JsTrue}
import org.apache.openwhisk.core.database.{ActivationHandler, UnsupportedQueryKeys, UnsupportedView, WhisksHandler}
import org.apache.openwhisk.core.entity.{UserLimits, WhiskQueries}
import org.apache.openwhisk.utils.JsHelpers
/**
* Maps the CouchDB view logic to expressed in javascript to Scala logic so as to enable
* performing queries by {{{MemoryArtifactStore}}}. Also serves as an example of what all query usecases
* are to be supported by any {{{ArtifactStore}}} implementation
*/
trait MemoryViewMapper {
protected val TOP: String = WhiskQueries.TOP
def filter(ddoc: String, view: String, startKey: List[Any], endKey: List[Any], d: JsObject, c: JsObject): Boolean
def sort(ddoc: String, view: String, descending: Boolean, s: Seq[JsObject]): Seq[JsObject]
protected def checkKeys(startKey: List[Any], endKey: List[Any]): Unit = {
require(startKey.nonEmpty)
require(endKey.nonEmpty)
require(startKey.head == endKey.head, s"First key should be same => ($startKey) - ($endKey)")
}
protected def equal(js: JsObject, name: String, value: String): Boolean =
JsHelpers.getFieldPath(js, name) match {
case Some(JsString(v)) => v == value
case _ => false
}
protected def isTrue(js: JsObject, name: String): Boolean =
JsHelpers.getFieldPath(js, name) match {
case Some(JsBoolean(v)) => v
case _ => false
}
protected def gte(js: JsObject, name: String, value: Number): Boolean =
JsHelpers.getFieldPath(js, name) match {
case Some(JsNumber(n)) => n.longValue >= value.longValue
case _ => false
}
protected def lte(js: JsObject, name: String, value: Number): Boolean =
JsHelpers.getFieldPath(js, name) match {
case Some(JsNumber(n)) => n.longValue <= value.longValue
case _ => false
}
protected def numericSort(s: Seq[JsObject], descending: Boolean, name: String): Seq[JsObject] = {
val f =
(js: JsObject) =>
JsHelpers.getFieldPath(js, name) match {
case Some(JsNumber(n)) => n.longValue
case _ => 0L
}
val order = implicitly[Ordering[Long]]
val ordering = if (descending) order.reverse else order
s.sortBy(f)(ordering)
}
}
private object ActivationViewMapper extends MemoryViewMapper {
private val NS = "namespace"
private val NS_WITH_PATH = ActivationHandler.NS_PATH
private val START = "start"
override def filter(ddoc: String,
view: String,
startKey: List[Any],
endKey: List[Any],
d: JsObject,
c: JsObject): Boolean = {
checkKeys(startKey, endKey)
val nsValue = startKey.head.asInstanceOf[String]
view match {
//whisks-filters ddoc uses namespace + invoking action path as first key
case "activations" if ddoc.startsWith("whisks-filters") =>
filterActivation(d, equal(c, NS_WITH_PATH, nsValue), startKey, endKey)
//whisks ddoc uses namespace as first key
case "activations" if ddoc.startsWith("whisks") => filterActivation(d, equal(d, NS, nsValue), startKey, endKey)
case _ => throw UnsupportedView(s"$ddoc/$view")
}
}
override def sort(ddoc: String, view: String, descending: Boolean, s: Seq[JsObject]): Seq[JsObject] =
view match {
case "activations" if ddoc.startsWith("whisks") => numericSort(s, descending, START)
case _ => throw UnsupportedView(s"$ddoc/$view")
}
private def filterActivation(d: JsObject, matchNS: Boolean, startKey: List[Any], endKey: List[Any]): Boolean = {
val filterResult = (startKey, endKey) match {
case (_ :: Nil, _ :: `TOP` :: Nil) =>
matchNS
case (_ :: (since: Number) :: Nil, _ :: `TOP` :: `TOP` :: Nil) =>
matchNS && gte(d, START, since)
case (_ :: (since: Number) :: Nil, _ :: (upto: Number) :: `TOP` :: Nil) =>
matchNS && gte(d, START, since) && lte(d, START, upto)
case _ => throw UnsupportedQueryKeys(s"$startKey, $endKey")
}
filterResult
}
}
private object WhisksViewMapper extends MemoryViewMapper {
private val NS = "namespace"
private val ROOT_NS = WhisksHandler.ROOT_NS
private val TYPE = "entityType"
private val UPDATED = "updated"
private val PUBLISH = "publish"
private val BINDING = "binding"
override def filter(ddoc: String,
view: String,
startKey: List[Any],
endKey: List[Any],
d: JsObject,
c: JsObject): Boolean = {
checkKeys(startKey, endKey)
val entityType = WhisksHandler.getEntityTypeForDesignDoc(ddoc, view)
val matchTypeAndView = equal(d, TYPE, entityType) && matchViewConditions(ddoc, view, d)
val matchNS = equal(d, NS, startKey.head.asInstanceOf[String])
val matchRootNS = equal(c, ROOT_NS, startKey.head.asInstanceOf[String])
//Here ddocs for actions, rules and triggers use
//namespace and namespace/packageName as first key
val filterResult = (startKey, endKey) match {
case (ns :: Nil, _ :: `TOP` :: Nil) =>
(matchTypeAndView && matchNS) || (matchTypeAndView && matchRootNS)
case (ns :: (since: Number) :: Nil, _ :: `TOP` :: `TOP` :: Nil) =>
(matchTypeAndView && matchNS && gte(d, UPDATED, since)) ||
(matchTypeAndView && matchRootNS && gte(d, UPDATED, since))
case (ns :: (since: Number) :: Nil, _ :: (upto: Number) :: `TOP` :: Nil) =>
(matchTypeAndView && matchNS && gte(d, UPDATED, since) && lte(d, UPDATED, upto)) ||
(matchTypeAndView && matchRootNS && gte(d, UPDATED, since) && lte(d, UPDATED, upto))
case _ => throw UnsupportedQueryKeys(s"$ddoc/$view -> ($startKey, $endKey)")
}
filterResult
}
private def matchViewConditions(ddoc: String, view: String, d: JsObject): Boolean = {
view match {
case "packages-public" if ddoc.startsWith("whisks") =>
isTrue(d, PUBLISH) && hasEmptyBinding(d)
case _ => true
}
}
private def hasEmptyBinding(js: JsObject) = {
js.fields.get(BINDING) match {
case Some(x: JsObject) if x.fields.nonEmpty => false
case _ => true
}
}
override def sort(ddoc: String, view: String, descending: Boolean, s: Seq[JsObject]): Seq[JsObject] = {
view match {
case "actions" | "rules" | "triggers" | "packages" | "packages-public" if ddoc.startsWith("whisks") =>
numericSort(s, descending, UPDATED)
case _ => throw UnsupportedView(s"$ddoc/$view")
}
}
}
private object SubjectViewMapper extends MemoryViewMapper {
private val BLOCKED = "blocked"
private val SUBJECT = "subject"
private val UUID = "uuid"
private val KEY = "key"
private val NS_NAME = "name"
override def filter(ddoc: String,
view: String,
startKey: List[Any],
endKey: List[Any],
d: JsObject,
c: JsObject): Boolean = {
require(startKey == endKey, s"startKey: $startKey and endKey: $endKey must be same for $ddoc/$view")
(ddoc, view) match {
case (s, "identities") if s.startsWith("subjects") =>
filterForMatchingSubjectOrNamespace(ddoc, view, startKey, endKey, d)
case ("namespaceThrottlings", "blockedNamespaces") =>
filterForBlacklistedNamespace(d)
case _ =>
throw UnsupportedView(s"$ddoc/$view")
}
}
private def filterForBlacklistedNamespace(d: JsObject): Boolean = {
val id = d.fields("_id")
id match {
case JsString(idv) if idv.endsWith("/limits") =>
val limits = UserLimits.serdes.read(d)
limits.concurrentInvocations.contains(0) || limits.invocationsPerMinute.contains(0)
case _ =>
d.getFields(BLOCKED) match {
case Seq(JsTrue) => true
case _ => false
}
}
}
private def filterForMatchingSubjectOrNamespace(ddoc: String,
view: String,
startKey: List[Any],
endKey: List[Any],
d: JsObject) = {
val notBlocked = !isTrue(d, BLOCKED)
startKey match {
case (ns: String) :: Nil => notBlocked && (equal(d, SUBJECT, ns) || matchingNamespace(d, equal(_, NS_NAME, ns)))
case (uuid: String) :: (key: String) :: Nil =>
notBlocked &&
(
(equal(d, UUID, uuid) && equal(d, KEY, key))
|| matchingNamespace(d, js => equal(js, UUID, uuid) && equal(js, KEY, key))
)
case _ => throw UnsupportedQueryKeys(s"$ddoc/$view -> ($startKey, $endKey)")
}
}
override def sort(ddoc: String, view: String, descending: Boolean, s: Seq[JsObject]): Seq[JsObject] = {
s //No sorting to be done
}
private def matchingNamespace(js: JsObject, matcher: JsObject => Boolean): Boolean = {
js.fields.get("namespaces") match {
case Some(JsArray(e)) => e.exists(v => matcher(v.asJsObject))
case _ => false
}
}
}