blob: b52b4dff3b3f53c3ad3a28a9771a436837be06db [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 java.util.regex.Matcher
import scala.util.Try
import spray.json.JsString
import spray.json.JsValue
import spray.json.RootJsonFormat
import spray.json.deserializationError
import org.apache.openwhisk.http.Messages
/**
* EntityPath is a path string of allowed characters. The path consists of parts each of which
* must be a valid EntityName, separated by the EntityPath separator character. The private
* constructor accepts a validated sequence of path parts and can reconstruct the path
* from it. The path cannot be empty.
*
* 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 path the sequence of parts that make up a namespace path
*/
protected[core] class EntityPath private (private val path: Seq[String]) extends AnyVal {
def namespace: String = path.foldLeft("")((a, b) => if (a != "") a.trim + EntityPath.PATHSEP + b.trim else b.trim)
/**
* @return number of parts in the path.
*/
def segments = path.length
/**
* Adds segment to path.
*/
def addPath(e: EntityName) = EntityPath(path :+ e.name)
/**
* Concatenates given path to existin path.
*/
def addPath(p: EntityPath) = EntityPath(path ++ p.path)
/**
* Computes the relative path by dropping the leftmost segment. The return is an option
* since dropping a singleton results in an invalid path.
*/
def relativePath: Option[EntityPath] = Try(EntityPath(path.drop(1))).toOption
/**
* @return the root of the path (the first segment).
*/
def root: EntityName = EntityName(path.head)
/**
* @return the last segment of the path.
*/
def last: EntityName = EntityName(path.last)
/**
* @return true iff the path contains exactly one segment (i.e., the namespace)
*/
def defaultPackage: Boolean = path.size == 1
/**
* Replaces root of this path with given namespace iff the root is
* the default namespace.
*/
def resolveNamespace(newNamespace: Namespace): EntityPath = {
resolveNamespace(newNamespace.name)
}
/**
* Replaces root of this path with given namespace iff the root is
* the default namespace.
*/
def resolveNamespace(newNamespace: EntityName): EntityPath = {
// check if namespace is default
if (root.toPath == EntityPath.DEFAULT) {
val newPath = path.updated(0, newNamespace.name)
EntityPath(newPath)
} else this
}
/**
* Converts the path to a fully qualified name. The path must contains at least 2 parts.
*
* @throws IllegalArgumentException if the path does not conform to schema (at least namespace and entity name must be present0
*/
@throws[IllegalArgumentException]
def toFullyQualifiedEntityName = {
require(path.size > 1, Messages.malformedFullyQualifiedEntityName)
val name = last
val newPath = EntityPath(path.dropRight(1))
FullyQualifiedEntityName(newPath, name)
}
def toDocId = DocId(namespace)
def toJson = JsString(namespace)
def asString = namespace // to make explicit that this is a string conversion
override def toString = namespace
}
protected[core] object EntityPath {
/** Path separator */
protected[core] val PATHSEP = "/"
/**
* Default namespace name. This name is not a valid entity name and is a special string
* that allows omission of the namespace during API calls. It is only used in the URI
* namespace extraction.
*/
protected[core] val DEFAULT = EntityPath("_")
/**
* Constructs a Namespace from a string. String must be a valid path, consisting of
* a valid EntityName separated by the Namespace separator character.
*
* @param path a valid namespace path
* @return Namespace for the path
* @throws IllegalArgumentException if the path does not conform to schema
*/
@throws[IllegalArgumentException]
protected[core] def apply(path: String): EntityPath = {
require(path != null, "path undefined")
val parts = path.split(PATHSEP).filter { _.nonEmpty }.toSeq
EntityPath(parts)
}
/**
* Namespace is a path string of allowed characters. The path consists of parts each of which
* must be a valid EntityName, separated by the Namespace separator character. The constructor
* accepts a sequence of path parts and can reconstruct the path from it.
*
* @param path the sequence of parts that make up a namespace path
* @throws IllegalArgumentException if any of the parts are not valid path part names
*/
@throws[IllegalArgumentException]
private def apply(parts: Seq[String]): EntityPath = {
require(parts != null && parts.nonEmpty, "path undefined")
require(parts.forall { s =>
s != null && EntityName.entityNameMatcher(s).matches
}, s"path contains invalid parts ${parts.toString}")
new EntityPath(parts)
}
/** Returns true iff the path is a valid namespace path. */
protected[core] def validate(path: String): Boolean = {
Try { EntityPath(path) } map { _ =>
true
} getOrElse false
}
implicit val serdes = new RootJsonFormat[EntityPath] {
def write(n: EntityPath) = n.toJson
def read(value: JsValue) =
Try {
val JsString(name) = value
EntityPath(name)
} getOrElse deserializationError("namespace malformed")
}
}
/**
* EntityName is a string of allowed characters.
*
* 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.
*/
protected[core] class EntityName private (val name: String) extends AnyVal {
def asString = name // to make explicit that this is a string conversion
def toJson = JsString(name)
def toPath: EntityPath = EntityPath(name)
def addPath(e: EntityName): EntityPath = toPath.addPath(e)
def addPath(e: Option[EntityName]): EntityPath = e map { toPath.addPath(_) } getOrElse toPath
override def toString = name
}
protected[core] object EntityName {
protected[core] val ENTITY_NAME_MAX_LENGTH = 256
/**
* Allowed path part or entity name format (excludes path separator): first character
* is a letter|digit|underscore, followed by one or more allowed characters in [\w@ .&-].
* The name may not have trailing white space.
*/
protected[core] val REGEX = raw"\A([\w]|[\w][\w@ .&-]{0,${ENTITY_NAME_MAX_LENGTH - 2}}[\w@.&-])\z"
private val entityNamePattern = REGEX.r.pattern // compile once
protected[core] def entityNameMatcher(s: String): Matcher = entityNamePattern.matcher(s)
/**
* Unapply method for convenience of case matching.
*/
protected[core] def unapply(name: String): Option[EntityName] = Try(EntityName(name)).toOption
/**
* EntityName is a string of allowed characters.
*
* @param name the entity name
* @throws IllegalArgumentException if the name does not conform to schema
*/
@throws[IllegalArgumentException]
protected[core] def apply(name: String): EntityName = {
require(name != null && entityNameMatcher(name).matches, s"name [$name] is not allowed")
new EntityName(name)
}
implicit val serdes = new RootJsonFormat[EntityName] {
def write(n: EntityName) = n.toJson
def read(value: JsValue) =
Try {
val JsString(name) = value
EntityName(name)
} getOrElse deserializationError("entity name malformed")
}
}