using concurrent::AtomicInt
using concurrent::AtomicRef
** (Service) -
** The top level IoC object that holds service definitions and the root scope.
**
** The 'Registry' instance may be dependency injected.
@Js
const mixin Registry {
** Destroys all active scopes and shuts down the registry.
abstract This shutdown()
** Returns the *root* scope.
**
** For normal IoC usage, consider using 'activeScope()' instead.
abstract Scope rootScope()
** Returns the global *default* scope.
** This is the default scope used in any new thread and defaults to the *root* scope.
**
** For normal IoC usage, consider using 'activeScope()' instead.
abstract Scope defaultScope()
** Returns the current *active* scope.
abstract Scope activeScope()
** Returns a map of all defined scopes, keyed by scope ID.
abstract Str:ScopeDef scopeDefs()
** Returns a map of all defined services, keyed by service ID.
abstract Str:ServiceDef serviceDefs()
** Returns a pretty printed list of service definitions.
** This is logged to standard out at registry startup.
** Remove the startup contribution to prevent the logging:
**
** pre>
** syntax: fantom
** regBuilder.onRegistryStartup() |Configuration config| {
** config.remove("afIoc.logServices")
** }
** <pre
abstract Str printServices()
** Returns the Alien-Factory ASCII art banner.
** This is logged to standard out at registry startup.
** Remove the startup contribution to prevent the logging:
**
** pre>
** syntax: fantom
** regBuilder.onRegistryStartup() |Configuration config| {
** config.remove("afIoc.logBanner")
** }
** <pre
abstract Str printBanner()
** *Advanced use only.*
**
** Sets a new global default scope and returns the old one.
** Only non-threaded scopes may be set as the global default.
abstract Scope setDefaultScope(Scope defaultScope)
** *Advanced use only.*
**
** Sets the given scope as the active scope in this thread.
** Call 'Scope.destroy()' to pop this scope off the active stack,
** or pass 'null' to this method to clear the active stack.
abstract Void setActiveScope(Scope? activeScope)
** Sets the global 'Registry' instance. (In essence, this just sets a static field to this
** 'Registry' instance.)
**
** To make sure the JVM is only running the one IoC container, this method throws an Err if
** the global instance has already been set.
**
** See `getGlobal`
abstract This setAsGlobal()
** Returns the global 'Registry' instance for this application.
** A global Registry lets class instances create / inject themselves;
** handy for when the IoC Container is not accessible
**
** pre>
** syntax: fantom
** class MyClass {
** @Inject MyService myService
**
** // private it-block ctor for IoC instantiation
** private new makeViaItBlock(|This| f) { f(this) }
**
** // public static ctor that creates itself via the global Registry
** static new make() {
** Registry.getGlobal.activeScope.build(MyClass#)
** }
** }
** <pre
**
** Now when you create an instance of 'MyClass' it comes fully loaded with injected services.
** Just don't forget to set a global Registry instance first.
**
** pre>
** syntax: fantom
** registry := RegistryBuilder() { .... }.build
** registry.setAsGlobal
**
** ...
**
**// myClass now comes fully loaded with injected services
** myClass := MyClass()
** <pre
**
** See `setAsGlobal`.
static Registry? getGlobal(Bool checked := true) {
RegistryImpl.globalRegRef.val ?: (checked ? throw Err(ErrMsgs.registry_globalNotSet) : null)
}
** Clears the global Registry instance.
@NoDoc
static Void clearGlobal() {
RegistryImpl.globalRegRef.val = null
}
}
@Js
internal const class RegistryImpl : Registry {
static
const AtomicInt instanceCount := AtomicInt(0)
const OneShotLock shuttingdownLock := OneShotLock(ErrMsgs.registry_alreadyShutdown, RegistryShutdownErr#)
const OneShotLock shutdownLock := OneShotLock(ErrMsgs.registry_alreadyShutdown, RegistryShutdownErr#)
const Str:ScopeDefImpl scopeDefs_
const Str:ServiceDefImpl serviceDefs_
const ScopeImpl rootScope_
const AutoBuilder autoBuilder // keep this handy for optimisation reasons
const Str:Str[] scopeIdLookup
const Str:[Type:Str[]] scopeTypeLookup
const ScopeImpl builtInScope
const Unsafe shutdownHooksRef
const RegistryMeta regMeta
const ActiveScopeStack activeScopeStack
const OperationsStack opStack
const AtomicRef defaultScopeRef := AtomicRef()
static const AtomicRef globalRegRef := AtomicRef(null)
override const Str:ScopeDef scopeDefs
override const Str:ServiceDef serviceDefs
new make(Duration buildStart, Str:ScpDef scopeDefs_, Str:SrvDef srvDefs, Type[] moduleTypes, [Str:Obj?] options, Func[] startupHooks, Func[] shutdownHooks) {
instanceCount.incrementAndGet
activeScopeStack = ActiveScopeStack(instanceCount.val)
opStack = OperationsStack(instanceCount.val)
this.shutdownHooksRef = Unsafe(shutdownHooks)
this.scopeDefs_ = scopeDefs_.map |def -> ScopeDefImpl| { def.toScopeDef }
scopeIdLookup := Str:Str[][:] { it.caseInsensitive = true }
scopeTypeLookup := Str:[Type:Str[]][:] { it.caseInsensitive = true }
this.scopeDefs_.each |scopeDef| {
idLookup := Str[,]
typeLookup := Type:Str[][:]
srvDefs.each |SrvDef srvDef| {
srvId := srvDef.id ?: ""
if (srvDef.matchesScope(scopeDef)) {
idLookup.add(srvId)
srvDef.serviceTypes.each {
typeLookup.getOrAdd(it) { Str[,] }.add(srvId)
}
srvDef.matchedScopes.add(scopeDef.id)
}
}
scopeIdLookup[scopeDef.id] = idLookup
scopeTypeLookup[scopeDef.id] = typeLookup
}
this.scopeIdLookup = scopeIdLookup
this.scopeTypeLookup = scopeTypeLookup
this.serviceDefs_ = srvDefs.map |srvDef->ServiceDefImpl| { srvDef.toServiceDef.validate(this) }
// sort scopeDefs and serviceDefs alphabetically - it's a slower lookup, so keep them in a different ref
scopeKeys := this.scopeDefs_.keys.sort
scopeDefs := Str:ScopeDef[:] { it.ordered = true }
scopeKeys.each { scopeDefs[it] = this.scopeDefs_[it] }
this.scopeDefs = scopeDefs
serviceKeys := this.serviceDefs_.keys.sort
serviceDefs := Str:ServiceDef[:] { it.ordered = true }
serviceKeys.each { serviceDefs[it] = this.serviceDefs_[it] }
this.serviceDefs = serviceDefs
// ---- Fire up the Scopes ----
now := Duration.now
buildDuration := now - buildStart
startStart := now
builtInScopeDef := findScopeDef("builtIn", null)
builtInScope = ScopeImpl(this, null, builtInScopeDef)
rootScopeDef := findScopeDef("root", builtInScope)
rootScope_ = ScopeImpl(this, builtInScope, rootScopeDef)
defaultScopeRef.val = rootScope_
// ---- Create Dependency Providers ----
// these are also redefined in IocModule
dependencyProviders := DependencyProviders(Str:DependencyProvider[:] { ordered = true }
.add("afIoc.autobuild", AutobuildProvider())
.add("afIoc.func", FuncProvider())
.add("afIoc.log", LogProvider())
.add("afIoc.scope", ScopeProvider())
.add("afIoc.config", ConfigProvider())
.add("afIoc.funcArg", FuncArgProvider())
.add("afIoc.service", ServiceProvider())
.add("afIoc.ctorItBlock", CtorItBlockProvider())
)
autoBuilder = AutoBuilder([:], dependencyProviders)
regMeta = RegistryMetaImpl(options, moduleTypes)
builtInScope.instanceById(Registry# .qname, [,], true).setInstance(this)
builtInScope.instanceById(RegistryMeta# .qname, [,], true).setInstance(regMeta)
builtInScope.instanceById(DependencyProviders# .qname, [,], true).setInstance(dependencyProviders)
builtInScope.instanceById(AutoBuilder# .qname, [,], true).setInstance(autoBuilder)
// it's chicken and egg - we need dependency providers to create dependency providers!
sysDepProInst := builtInScope.instanceById(DependencyProviders#.qname, [,], true)
userDepPro := (DependencyProviders) autoBuilder.autobuild(rootScope_, DependencyProviders#, null, null, DependencyProviders#.qname)
sysDepProInst.setInstance(userDepPro)
autoBuilderInst := builtInScope.instanceById(AutoBuilder#.qname, [,], true)
autoBuilder = autoBuilder.autobuild(rootScope_, AutoBuilder#, [userDepPro], null, AutoBuilder#.qname)
autoBuilderInst.setInstance(autoBuilder)
// ---- Startup Registry ----
config := ConfigurationImpl(rootScope_, Str:|Scope|#, "afIoc::Registry.onStartup")
startupHooks.each {
it.call(config)
config.cleanupAfterMethod
}
hooks := (Str:Func) config.toMap
// ensure system messages are printed at the end
order := ("afIoc.logBanner " + options.get("afIoc.afterStartup", "")).split
hooks.keys.sort |k1, k2| {
(order.index(k1) ?: -1) <=> (order.index(k2) ?: -1)
}.each {
hooks[it].call(rootScope_)
}
if (hooks.containsKey("afIoc.logStartupTimes")) {
startDuration := Duration.now - startStart
buildTime := buildDuration.toMillis.toLocale("#,###")
startupTime := startDuration.toMillis.toLocale("#,###")
msg := "IoC Registry built in ${buildTime}ms and started up in ${startupTime}ms"
Registry#.pod.log.info(msg)
}
}
override This shutdown() {
if (shuttingdownLock.lock) return this
// call the Shutdown hooks first so services (and shutdown contributions!) can still access the registry
then := Duration.now
config := ConfigurationImpl(rootScope_, Str:|Scope|#, "afIoc::Registry.onShutdown")
configs := (Func[]) shutdownHooksRef.val
configs.each {
it.call(config)
config.cleanupAfterMethod
}
hooks := (Str:Func) config.toMap
sayGoodbye := hooks.containsKey("afIoc.sayGoodbye")
hooks.each { it.call(rootScope_) }
// destroy all active scopes and their children...!
scope := (ScopeImpl?) activeScope
sdErrs := Err[,]
while (scope != null) {
sdErrs.addAll(scope._destroy ?: Err#.emptyList)
scope = scope.parent
}
// ensure the root and default scopes are destroyed
// for wotever reason they may not have been part of the active scope hierarchy
scope = defaultScopeRef.val
while (scope != null) {
sdErrs.addAll(scope._destroy ?: Err#.emptyList)
scope = scope.parent
}
scope = rootScope_
while (scope != null) {
sdErrs.addAll(scope._destroy ?: Err#.emptyList)
scope = scope.parent
}
if (sayGoodbye) {
log := Registry#.pod.log
shutdownTime := (Duration.now - then).toMillis.toLocale("#,###")
log.info("IoC shutdown in ${shutdownTime}ms")
log.info("IoC says, \"Goodbye!\"")
}
// allow services (and shutdown contributions!) access the registry until it *has* been shutdown
shutdownLock.lock
if (sdErrs.size > 0)
throw sdErrs.first
return this
}
override Scope rootScope() {
shutdownLock.check
return rootScope_
}
override Scope defaultScope() {
shutdownLock.check
return defaultScopeRef.val
}
override Scope activeScope() {
shutdownLock.check
return activeScopeStack.peek ?: defaultScopeRef.val
}
override Str printServices() {
print := "\n"
groups := groupBy(serviceDefs.vals) |ServiceDef def->Obj?| { def.type.pod.name }
buckets := (Str:ServiceDef[]) groups.keys.sort.reduce(Str:ServiceDef[][:] { it.ordered = true }) |Str:ServiceDef[] map, key| { map[key] = groups[key] }
maxSize := 0
buckets.each |ServiceDef[] serviceDefs, Str podName| {
serSize := (Int) serviceDefs.reduce(0) |size, stat| { ((Int) size).max(stat.id.replace("${podName}::", "").size) }
maxSize = maxSize.max(serSize)
}
built := 0
buckets.each |ServiceDef[] serviceDefs, Str podName| {
srvcs := ""
noOfPub := 0
noOfPri := 0
serviceDefs.each |ServiceDefImpl def| {
pub := def.serviceTypes.any { isPublic }
if (pub) {
sep := def.noOfInstancesBuilt > 0 ? "|" : ":"
srvcs += def.id.replace("${podName}::", "").padl(maxSize + 2) + "${sep} " + def.matchedScopes.join(", ")
alias := def.aliases.dup.addAll(def.aliasTypes.map { it.qname })
if (alias.size > 0)
srvcs += " (aliases: " + alias.join(", ") + ")"
srvcs += "\n"
noOfPub++
} else
noOfPri++
if (def.noOfInstancesBuilt > 0) built++
}
print += noOfPub == 1
? "\nPod '${podName}' has 1 public service"
: "\nPod '${podName}' has ${noOfPub} public services"
if (noOfPri > 0)
print += " (and ${noOfPri} internal)"
print += ":\n\n"
print += srvcs
}
stats := serviceDefs.vals
perce := (100f * built / stats.size).toLocale("0.00")
print += "\n${perce}% of services were built on startup (${built}/${stats.size})\n"
return print
}
// see http://fantom.org/forum/topic/2296
static Obj:Obj[] groupBy(Obj[] list, |Obj item, Int index->Obj| keyFunc) {
list.reduce(Obj:Obj[][:] { it.ordered = true}) |Obj:Obj[] bucketList, val, i| {
key := keyFunc(val, i)
bucketList.getOrAdd(key) { Obj[,] }.add(val)
return bucketList
}
}
override Str printBanner() {
heading := (Str) (regMeta.options["afIoc.bannerText"] ?: "Err...")
title := "\n"
title += Str<| ___ __ _____ __
/ _ | / /____ _____ / ___/_ ____/ /_________ __ __
/ _ | / / / -_|/ _ /===/ __/ _ \/ __/ __/ _ / __|/ // /
/_/ |_|/_/_/\__|/_//_/ /_/ \_,_/___/\__/____/_/ \_, /
|>
first := true
while (!heading.isEmpty) {
banner := heading.size > 52 ? heading[0..<52] : heading
heading = heading[banner.size..-1]
banner = first ? (banner.padl(52, ' ') + " /___/ \n") : (banner.padr(52, ' ') + "\n")
title += banner
first = false
}
return title
}
override Scope setDefaultScope(Scope scope) {
if (scope.isThreaded)
throw ArgErr("Scope '${scope.id}' is threaded. Only non-threaded scopes may be set as the global default.")
oldScope := this.defaultScopeRef.val
this.defaultScopeRef.val = scope
return oldScope
}
override Void setActiveScope(Scope? activeScope) {
if (activeScope == null)
activeScopeStack.clear
else
activeScopeStack.push(activeScope)
}
override This setAsGlobal() {
if (globalRegRef.val != null)
throw Err(ErrMsgs.registry_globalAlreadySet)
globalRegRef.val = this
return this
}
ScopeDefImpl findScopeDef(Str scopeId, ScopeImpl? currentScope) {
scopeDef := (ScopeDefImpl) (scopeDefs_.find |def| { def.matchesId(scopeId) } ?: throw ArgNotFoundErr(ErrMsgs.scope_scopeNotFound(scopeId), scopeDefs_.keys))
scope := currentScope
scopes := ScopeImpl[,]
if (scope != null) scopes.add(scope)
while (scope?.parent != null) {
scope = scope.parent
scopes.insert(0, scope)
}
// there's no technical reason to disallow scope nesting, but I can't think of a reason why you would want it!?
// Ergo, it's probably a user error.
if (scopes.any { it.scopeDef.matchesId(scopeId) })
throw IocErr(ErrMsgs.scope_scopesMayNotBeNested(scopeId, scopes.map { it.id }))
if (currentScope != null && !scopeDef.threaded && currentScope.scopeDef.threaded)
throw IocErr(ErrMsgs.scope_invalidScopeNesting(scopeId, currentScope.id))
return scopeDef
}
}