com.reactific.helpers.Registration.scala Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2015-2017 Reactific Software LLC
*
* Licensed 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 com.reactific.helpers
import java.util.concurrent.ConcurrentHashMap
import scala.collection.JavaConverters._
import scala.language.postfixOps
import scala.util.Random
trait Identifiable {
def id: Symbol
lazy val label: String = id.name
}
trait IdentifiedWithRegistry extends Identifiable {
def registry: Registry[_]
def registryName: String = registry.registryName
}
/** Mix this in to anything you want to register and define the "id";
* then pass that object to the Registrar you want
*/
// scalastyle:off
trait Registrable[T <: Registrable[T]]
extends IdentifiedWithRegistry with LoggingHelper { self: T ⇒
def registry: Registry[T]
def register(): Unit = registry.register(self)
def unregister(): Unit = registry.unregister(self)
def isRegistered: Boolean = registry.isRegistered(id)
def reference: RegistryReference[T] = RegistryReference(registry, id)
override def finalize(): Unit = { // scalastyle:ignore
try {
if (isRegistered) unregister()
} catch {
case xcptn: Throwable ⇒
/* ignore exceptions to always garbage collect the object */
log.trace(
s"Ignoring failure to unregister $label at finalize so " +
"exception doesn't thwart GC. Cause:",
xcptn
)
}
}
register()
}
// scalastyle:on
/** Abstract Registry Of Key-Value Pairs
* This trait implements a key-value map using ConcurrentHashMap to minimize
* lock contention for multiple threads accessing the registry. It is
* intended to identify race conditions upon insertion and deletion of entries.
* @tparam K The type of Key
* @tparam V The type of Value
*/
trait AbstractRegistry[K <: Any, V <: AnyRef]
extends LoggingHelper with ThrowingHelper {
def contains(key: K): Boolean = _registrants.contains(key)
def exists(name: K): Boolean = _registrants.contains(name)
def containsValue(value: V): Boolean = _registrants.values.exists { item ⇒
item == value
}
def lookup(key: K): Option[V] = _registrants.get(key)
def lookupOrElse(key: K, value: V): Option[V] = {
_registrants.putIfAbsent(key, value)
_registrants.get(key)
}
def pick(index: Int): K = {
require(
index >= 0 && index < size,
s"Registrant index ($index) out of range [0-$size)"
)
_registrants.keySet.toSeq(index)
}
def size: Int = _registrants.size
def isEmpty: Boolean = _registrants.isEmpty
def nonEmpty: Boolean = _registrants.nonEmpty
def values: Seq[V] = _registrants.values.toSeq
def keys: Seq[K] = _registrants.keys.toSeq
def select(f: ((K, V)) => Boolean): Iterable[V] = {
for { (k, v) <- _registrants if f((k, v)) } yield { v }
}
def map[W](f: ((K, V)) => (K, W)): Map[K, W] = {
_registrants.map { (x) =>
f(x)
} toMap
}
protected final def _register(key: K, obj: V): obj.type = {
_registrants.putIfAbsent(key, obj) match {
case Some(otherObj) =>
if (otherObj != obj) {
toss(
s"Insertion of $key was preempted by another thread. " +
"Fix your race condition."
)
} else {
log.warn(
s"Preemptive replacement of $key yielding same value is " +
"indicative of race condition."
)
obj
}
case None =>
obj
}
}
protected final def _unregister(key: K): Unit = {
_registrants.remove(key) match {
case Some(_) =>
// ignore
case None =>
toss(
s"Removal of $key was preempted by another thread. " +
s"Fix your race condition."
)
}
}
private[this] val _registrants = new ConcurrentHashMap[K, V] asScala
}
/** A trait for specifying the registration of some type of object (T)
* which must have at least Registrable mixed in.
* This abstracts the notion of a registry of objects that conform to an
* interface.
*/
trait Registry[T <: Registrable[T]] extends AbstractRegistry[Symbol, T] {
def registryName: String
def registrantsName: String
override protected def createLoggerName = s"${registryName}Registry"
def isRegistered(name: Symbol): Boolean = contains(name)
def getRegistrant(name: Symbol): Option[T] = lookup(name)
def apply(name: Symbol): Option[T] = lookup(name)
def find(ids: Seq[Symbol]): Seq[T] = {
values.filter { t ⇒
ids.contains(t.id)
}
}
def as[U <: Registrable[T]](id: Symbol): U = {
this(id) match {
case Some(typ) ⇒ typ.asInstanceOf[U]
case None ⇒ toss(s"Could not find $registrantsName named '$id'")
}
}
def register(thing: T): Unit = {
val theName = s""""${thing.id.name}"""" // scalastyle:ignore
// If id is null, let's fail before we do anything
if (contains(thing.id)) {
toss(
s"There is already a $registrantsName " +
s"named $theName registered with $registryName"
)
}
_register(thing.id, thing)
log.trace(
s"Registered ${thing.getClass.getName} as $theName " +
s"with $size other ${Pluralizer.pluralize(registrantsName)}"
)
}
def unregister(thing: T): Unit = {
val theName = s""""${thing.id.name}"""" // scalastyle:ignore
// If id is null, let's fail before we do anything
if (!contains(thing.id)) {
toss(
s"There is no $registrantsName named $theName " +
s"registered with $registryName"
)
}
_unregister(thing.id)
log.trace(
s"Unregistered ${thing.getClass.getName} as $theName with " +
s"$size other ${Pluralizer.pluralize(registrantsName)}"
)
}
val rand = new Random(System.currentTimeMillis())
def pick: T = {
val random_index =
if (size == 0) {
toss("Can't pick from an empty registry")
} else {
rand.nextInt(size)
}
val key: Symbol = pick(random_index)
getRegistrant(key).get
}
}
/** Reference A Memory Object From The Database
*
* Many of the objects used in Scrupal are not stored in the database.
* Essentially those things coming from a Module are just retained in
* memory: Module, Feature, Entity, Type, Handler, etc. However, we need to
* reference these things from objects that are stored in the database. We
* do not want to be loading the low level memory objects all the time but
* would just like to "find" them. That's what the RegistryReference is for.
* We can register long-lived immutable objects in a registry and find them
* later, by name. But, there is a registry per object type, by design. So,
* we need a way to capture a reference to a named object in one of the
* registries. We do that by simply caching the registry object and
* the registrable object's id. This information is then stored in the
* database and reconstructed into the referenced object with the apply method.
*/
case class RegistryReference[T <: Registrable[T]] private[helpers] (
registry: Registry[T],
id: Symbol)
extends (() ⇒ Option[T]) {
def as[C](): Option[C] = this.apply().map { x ⇒
x.asInstanceOf[C]
}
def apply(): Option[T] = {
registry.lookup(id)
}
}
object RegistryReference {
def to[T <: Registrable[T]](o: T): RegistryReference[T] = {
RegistryReference[T](o.registry, o.id)
}
}