blob: f7669676607fdb92a2c6319cc62d0713d82b8aef [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
import scala.util.Success
import scala.util.Try
import spray.json._
import org.apache.openwhisk.core.entity.size.SizeString
/**
* A FullyQualifiedEntityName (qualified name) is a triple consisting of
* - EntityPath: the namespace and package where the entity is located
* - EntityName: the name of the entity
* - Version: the semantic version of the resource
* - Binding : the entity path of the package binding, it can be used by entities that support binding
*/
protected[core] case class FullyQualifiedEntityName(path: EntityPath,
name: EntityName,
version: Option[SemVer] = None,
binding: Option[EntityPath] = None)
extends ByteSizeable {
private val qualifiedName: String = path + EntityPath.PATHSEP + name
/** Resolves default namespace in path to given name if the root path is the default namespace. */
def resolve(namespace: EntityName) = FullyQualifiedEntityName(path.resolveNamespace(namespace), name, version)
/** @return full path including name, i.e., "path/name" */
def fullPath: EntityPath = path.addPath(name)
/**
* Creates new fully qualified entity name that shifts the name into the path and adds a new name:
* (p, n).add(x) -> (p/n, x).
*
* @return new fully qualified name
*/
def add(n: EntityName) = FullyQualifiedEntityName(path.addPath(name), n)
def toDocId = new DocId(qualifiedName)
def namespace: EntityName = path.root
def qualifiedNameWithLeadingSlash: String = EntityPath.PATHSEP + qualifiedName
def asString = path.addPath(name) + version.map("@" + _.toString).getOrElse("")
def serialize = FullyQualifiedEntityName.serdes.write(this).compactPrint
override def size = qualifiedName.sizeInBytes
override def toString = asString
override def hashCode = qualifiedName.hashCode
}
protected[core] object FullyQualifiedEntityName extends DefaultJsonProtocol {
// must use jsonFormat with explicit field names and order because class extends a trait
private val caseClassSerdes = jsonFormat(FullyQualifiedEntityName.apply _, "path", "name", "version", "binding")
protected[core] val serdes = new RootJsonFormat[FullyQualifiedEntityName] {
def write(n: FullyQualifiedEntityName) = caseClassSerdes.write(n)
def read(value: JsValue) =
Try {
value match {
case JsObject(fields) => caseClassSerdes.read(value)
// tolerate dual serialization modes; Exec serializes a sequence of fully qualified names
// by their document id which excludes the version (hence it is just a string)
case JsString(name) => EntityPath(name).toFullyQualifiedEntityName
case _ => deserializationError("fully qualified name malformed")
}
} match {
case Success(s) => s
case Failure(t: IllegalArgumentException) => deserializationError(t.getMessage)
case Failure(t) => deserializationError("fully qualified name malformed")
}
}
// alternate serializer that drops version
protected[entity] val serdesAsDocId = new RootJsonFormat[FullyQualifiedEntityName] {
def write(n: FullyQualifiedEntityName) = n.toDocId.toJson
def read(value: JsValue) =
Try {
value match {
case JsString(name) => EntityPath(name).toFullyQualifiedEntityName
case _ => deserializationError("fully qualified name malformed")
}
} match {
case Success(s) => s
case Failure(t: IllegalArgumentException) => deserializationError(t.getMessage)
case Failure(t) => deserializationError("fully qualified name malformed")
}
}
protected[core] def parse(msg: String) = Try(serdes.read(msg.parseJson))
/**
* Converts the name to a fully qualified name.
* There are 3 cases:
* - name is not a valid EntityPath => error
* - name is a valid single segment with a leading slash => error
* - name is a valid single segment without a leading slash => map it to user namespace, default package
* - name is a valid multi segment with a leading slash => treat it as fully qualified name (max segments allowed: 3)
* - name is a valid multi segment without a leading slash => treat it as package name and resolve it to the user namespace (max segments allowed: 3)
*
* The last case is ambiguous as '/namespace/action' and 'package/action' will be the same EntityPath value.
* The action should use a fully qualified result to avoid the ambiguity.
*
* @param name name of the action to fully qualify
* @param namespace the user namespace for the simple resolution
* @return Some(FullyQualifiedName) if the name is valid otherwise None
*/
protected[core] def resolveName(name: JsValue, namespace: EntityName): Option[FullyQualifiedEntityName] = {
name match {
case v @ JsString(s) =>
Try(v.convertTo[EntityPath]).toOption
.flatMap { path =>
val n = path.segments
val leadingSlash = s.startsWith(EntityPath.PATHSEP)
if (n < 1 || n > 3 || (leadingSlash && n == 1) || (!leadingSlash && n > 3)) None
else if (leadingSlash || n == 3) Some(path)
else Some(namespace.toPath.addPath(path))
}
.map(_.resolveNamespace(namespace).toFullyQualifiedEntityName)
case _ => None
}
}
}