├── .gitignore ├── README.md └── ioc.cfc /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | .settings 3 | mxunit 4 | tests/report 5 | tests/results 6 | WEB-INF 7 | settings.xml 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | DI/1 is a simple convention-based dependency injection (inversion of control) 2 | framework. DI/1 is part of the [FW/1 family of components](https://github.com/framework-one/fw1). 3 | All development work occurs in that repo. This repo is provided purely as a convenience for users 4 | who want just DI/1 as a standalone component. 5 | 6 | Usage 7 | ---- 8 | Initialize it with a comma-separated list of folders to scan for CFCs: 9 | 10 | var beanfactor = new ioc("/model,/shared/services"); 11 | 12 | CFCs found in a folder called `beans` are assumed to be transient. All other CFCs 13 | are assumed to be singletons. This can be overridden via optional configuration. 14 | 15 | DI/1 supports constructor injection, setter injection and property-based injection. 16 | All injection is done by name. If a bean name is unique, it can be used as-is, else 17 | the bean will have an alias which is the bean name followed by its immediate parent 18 | folder name, e.g., 19 | 20 | /model/beans/user.cfc will be "user" and "userBean" 21 | /model/services/product.cfc will be "product" and "productService" 22 | 23 | Folder names may be singular or plural. DI/1 assumes that if a folder name ends in 24 | "s" it can remove that to get the singular name. 25 | 26 | See the [DI/1 documentation](https://framework-one.github.io/documentation/4.3/using-di-one/) for more details. 27 | -------------------------------------------------------------------------------- /ioc.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | variables._fw1_version = "4.3.0"; 3 | variables._di1_version = variables._fw1_version; 4 | /* 5 | Copyright (c) 2010-2018, Sean Corfield 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | // CONSTRUCTOR 21 | 22 | public any function init( any folders, struct config = { } ) { 23 | variables.folderList = folders; 24 | variables.folderArray = folders; 25 | if ( isSimpleValue( folders ) ) { 26 | variables.folderArray = listToArray( folders ); 27 | } else { 28 | variables.folderList = arrayToList( folders ); 29 | } 30 | var n = arrayLen( variables.folderArray ); 31 | for ( var i = 1; i <= n; ++i ) { 32 | var folderName = trim( variables.folderArray[ i ] ); 33 | // strip trailing slash since it can cause weirdness in path 34 | // deduction on some engines on some platforms (guess which!) 35 | if ( len( folderName ) > 1 && 36 | ( right( folderName, 1 ) == '/' || 37 | right( folderName, 1 ) == chr(92) ) ) { 38 | folderName = left( folderName, len( folderName ) - 1 ); 39 | } 40 | variables.folderArray[ i ] = folderName; 41 | } 42 | variables.config = config; 43 | variables.beanInfo = { }; 44 | variables.beanCache = { }; 45 | variables.resolutionCache = { }; 46 | variables.getBeanCache = { }; 47 | variables.accumulatorCache = { }; 48 | variables.initMethodCache = { }; 49 | variables.settersInfo = { }; 50 | variables.autoExclude = [ 51 | '/WEB-INF', '/Application.cfc', // never manage these! 52 | // assume default name for intermediary: 53 | '/MyApplication.cfc', 54 | 'framework.cfc', 'ioc.cfc', // legacy FW/1 / DI/1 55 | // recent FW/1 + DI/1 + AOP/1 exclusions: 56 | '/framework/aop.cfc', '/framework/beanProxy.cfc', 57 | '/framework/ioc.cfc', '/framework/WireBoxAdapter.cfc', 58 | '/framework/one.cfc' 59 | ]; 60 | variables.listeners = 0; 61 | setupFrameworkDefaults(); 62 | if ( structKeyExists( variables.config, 'loadListener' ) ) { 63 | this.onLoad( variables.config.loadListener ); 64 | } 65 | return this; 66 | } 67 | 68 | // PUBLIC METHODS 69 | 70 | // programmatically register an alias 71 | public any function addAlias( string aliasName, string beanName ) { 72 | discoverBeans(); // still need this since we rely on beanName having been discovered :( 73 | variables.beanInfo[ aliasName ] = variables.beanInfo[ beanName ]; 74 | return this; 75 | } 76 | 77 | 78 | // programmatically register new beans with the factory (add a singleton name/value pair) 79 | public any function addBean( string beanName, any beanValue ) { 80 | variables.beanInfo[ beanName ] = { 81 | name = beanName, value = beanValue, isSingleton = true 82 | }; 83 | return this; 84 | } 85 | 86 | 87 | // return true if the factory (or a parent factory) knows about the requested bean 88 | public boolean function containsBean( string beanName ) { 89 | discoverBeans(); 90 | return structKeyExists( variables.beanInfo, beanName ) || 91 | ( hasParent() && variables.parent.containsBean( beanName ) ); 92 | } 93 | 94 | 95 | // builder syntax for declaring new beans 96 | public any function declare( string beanName ) { 97 | var declaration = { beanName : beanName, built : false }; 98 | var beanFactory = this; // to make the builder functions less confusing 99 | structAppend( declaration, { 100 | // builder for addAlias() 101 | aliasFor : function( string beanName ) { 102 | if ( declaration.built ) throw "Declaration builder already completed!"; 103 | declaration.built = true; 104 | beanFactory.addAlias( declaration.beanName, beanName ); 105 | return declaration; 106 | }, 107 | // builder for addBean() 108 | asValue : function( any beanValue ) { 109 | if ( declaration.built ) throw "Declaration builder already completed!"; 110 | declaration.built = true; 111 | beanFactory.addBean( declaration.beanName, beanValue ); 112 | return declaration; 113 | }, 114 | // builder for factoryBean() 115 | fromFactory : function( any factory, string methodName = "" ) { 116 | if ( declaration.built ) throw "Declaration builder already completed!"; 117 | declaration.built = true; 118 | // use defaults -- we can override later 119 | beanFactory.factoryBean( declaration.beanName, factory, methodName ); 120 | return declaration; 121 | }, 122 | // builder for declareBean() 123 | instanceOf : function( string dottedPath ) { 124 | if ( declaration.built ) throw "Declaration builder already completed!"; 125 | declaration.built = true; 126 | // use defaults -- we can override later 127 | beanFactory.declareBean( declaration.beanName, dottedPath ); 128 | return declaration; 129 | }, 130 | // modifiers for metadata 131 | asSingleton : function() { 132 | if ( !declaration.built ) throw "No declaration builder to modify!"; 133 | variables.beanInfo[ declaration.beanName ].isSingleton = true; 134 | return declaration; 135 | }, 136 | asTransient : function() { 137 | if ( !declaration.built ) throw "No declaration builder to modify!"; 138 | variables.beanInfo[ declaration.beanName ].isSingleton = false; 139 | return declaration; 140 | }, 141 | withArguments : function( array args ) { 142 | if ( !declaration.built ) throw "No declaration builder to modify!"; 143 | var info = variables.beanInfo[ declaration.beanName ]; 144 | if ( !structKeyExists( info, 'factory' ) ) throw "withArguments() requires fromFactory()!"; 145 | info.args = args; 146 | return declaration; 147 | }, 148 | withOverrides : function( struct overrides ) { 149 | if ( !declaration.built ) throw "No declaration builder to modify!"; 150 | var info = variables.beanInfo[ declaration.beanName ]; 151 | if ( !structKeyExists( info, 'factory' ) && 152 | !structKeyExists( info, 'cfc' ) ) throw "withOverrides() requires fromFactory() or instanceOf()!"; 153 | info.overrides = overrides; 154 | return declaration; 155 | }, 156 | // to allow chaining 157 | done : function() { 158 | return beanFactory; 159 | } 160 | } ); 161 | return declaration; 162 | } 163 | 164 | 165 | // programmatically register new beans with the factory (add an actual CFC) 166 | public any function declareBean( string beanName, string dottedPath, boolean isSingleton = true, struct overrides = { } ) { 167 | var singleDir = ''; 168 | if ( listLen( dottedPath, '.' ) > 1 ) { 169 | var cfc = listLast( dottedPath, '.' ); 170 | var dottedPart = left( dottedPath, len( dottedPath ) - len( cfc ) - 1 ); 171 | singleDir = singular( listLast( dottedPart, '.' ) ); 172 | } 173 | var basePath = replace( dottedPath, '.', '/', 'all' ); 174 | var cfcPath = expandPath( '/' & basePath & '.cfc' ); 175 | var expPath = cfcPath; 176 | if ( !fileExists( expPath ) ) throw "Unable to find source file for #dottedPath#: expands to #cfcPath#"; 177 | var cfcPath = replace( expPath, chr(92), '/', 'all' ); 178 | var metadata = { 179 | name = beanName, qualifier = singleDir, isSingleton = isSingleton, 180 | path = cfcPath, cfc = dottedPath, metadata = cleanMetadata( dottedPath ), 181 | overrides = overrides 182 | }; 183 | variables.beanInfo[ beanName ] = metadata; 184 | return this; 185 | } 186 | 187 | public any function factoryBean( string beanName, any factory, string methodName = "", array args = [ ], struct overrides = { } ) { 188 | var metadata = { 189 | name = beanName, isSingleton = false, // really? 190 | factory = factory, method = methodName, args = args, 191 | overrides = overrides 192 | }; 193 | variables.beanInfo[ beanName ] = metadata; 194 | return this; 195 | } 196 | 197 | 198 | // return the requested bean, fully populated 199 | public any function getBean( string beanName, struct constructorArgs = { } ) { 200 | discoverBeans(); 201 | if ( structKeyExists( variables.beanInfo, beanName ) ) { 202 | if ( structKeyExists( variables.getBeanCache, beanName ) ) { 203 | return variables.getBeanCache[ beanName ]; 204 | } 205 | var bean = resolveBean( beanName, constructorArgs ); 206 | if ( isSingleton( beanName ) ) variables.getBeanCache[ beanName ] = bean; 207 | return bean; 208 | } else if ( hasParent() ) { 209 | // ideally throw an exception for non-DI/1 parent when args passed 210 | // WireBox adapter can do that since we control it but we can't do 211 | // anything for other bean factories - will revisit before release 212 | return variables.parent.getBean( beanName, constructorArgs ); 213 | } else { 214 | return missingBean( beanName = beanName, dependency = false ); 215 | } 216 | } 217 | 218 | // convenience API for metaprogramming perhaps? 219 | public any function getBeanInfo( string beanName = '', boolean flatten = false, 220 | string regex = '' ) { 221 | discoverBeans(); 222 | if ( len( beanName ) ) { 223 | // ask about a specific bean: 224 | if ( structKeyExists( variables.beanInfo, beanName ) ) { 225 | return variables.beanInfo[ beanName ]; 226 | } 227 | if ( hasParent() ) { 228 | return parentBeanInfo( beanName ); 229 | } 230 | throw 'bean not found: #beanName#'; 231 | } else { 232 | var result = { beanInfo = { } }; 233 | if ( hasParent() ) { 234 | if ( flatten || len( regex ) ) { 235 | structAppend( result.beanInfo, parentBeanInfoList( flatten ).beanInfo ); 236 | structAppend( result.beanInfo, variables.beanInfo ); 237 | } else { 238 | result.beanInfo = variables.beanInfo; 239 | result.parent = parentBeanInfoList( flatten ); 240 | }; 241 | } else { 242 | result.beanInfo = variables.beanInfo; 243 | } 244 | if ( len( regex ) ) { 245 | var matched = { }; 246 | for ( var name in result.beanInfo ) { 247 | if ( REFind( regex, name ) ) { 248 | matched[ name ] = result.beanInfo[ name ]; 249 | } 250 | } 251 | result.beanInfo = matched; 252 | } 253 | return result; 254 | } 255 | } 256 | 257 | 258 | // return a copy of the DI/1 configuration 259 | public struct function getConfig() { 260 | // note: we only make a shallow copy 261 | return structCopy( variables.config ); 262 | } 263 | 264 | 265 | // return the DI/1 version 266 | public string function getVersion() { 267 | return variables.config.version; 268 | } 269 | 270 | 271 | // return true if this factory has a parent 272 | public boolean function hasParent() { 273 | return structKeyExists( variables, 'parent' ); 274 | } 275 | 276 | 277 | // return true iff bean is known to be a singleton 278 | public boolean function isSingleton( string beanName ) { 279 | discoverBeans(); 280 | if ( structKeyExists( variables.beanInfo, beanName ) ) { 281 | return variables.beanInfo[ beanName ].isSingleton; 282 | } else if ( hasParent() ) { 283 | try { 284 | return variables.parent.isSingleton( beanName ); 285 | } catch ( any e ) { 286 | return false; // parent doesn't know the bean therefore is it not singleton 287 | } 288 | } else { 289 | return false; // we don't know the bean therefore it is not a managed singleton 290 | } 291 | } 292 | 293 | 294 | /* 295 | * @hint Given a bean (by name, by type or by value), call the named setters with the specified property values 296 | * @ignoreMissing When set verify that the setter to be called exists and skip if missing, otherwise throws an error 297 | */ 298 | public any function injectProperties( any bean, struct properties, boolean ignoreMissing=false ) { 299 | if ( isSimpleValue( bean ) ) { 300 | if ( containsBean( bean ) ) bean = getBean( bean ); 301 | else bean = construct( bean ); 302 | } 303 | for ( var property in properties ) { 304 | if ( !isNull( properties[ property ] ) && (!ignoreMissing || structKeyExists( bean, "set#property#" ) ) ){ 305 | var args = { }; 306 | args[ property ] = properties[ property ]; 307 | invoke( bean, "set#property#", args ); 308 | } 309 | } 310 | return bean; 311 | } 312 | 313 | 314 | // empty the cache and reload all the singleton beans 315 | // note: this does not reload the parent - if you have parent/child factories you 316 | // are responsible for dealing with that logic (it's safe to reload a child but 317 | // if you reload the parent, you must reload *all* child factories to ensure 318 | // things stay consistent!) 319 | public any function load() { 320 | discoverBeans(); 321 | variables.beanCache = { }; 322 | variables.resolutionCache = { }; 323 | variables.accumulatorCache = { }; 324 | variables.getBeanCache = { }; 325 | variables.initMethodCache = { }; 326 | for ( var key in variables.beanInfo ) { 327 | if ( !structKeyExists( variables.beanInfo[ key ], "isSingleton" ) ) 328 | throw "internal error: bean #key# has no isSingleton flag!"; 329 | if ( variables.beanInfo[ key ].isSingleton ) { 330 | getBean( key ); 331 | } 332 | } 333 | return this; 334 | } 335 | 336 | 337 | // add a listener for processing after a (re)load of the factory 338 | // called with just the factory, should be a plain function 339 | public any function onLoad( any listener ) { 340 | var head = { next = variables.listeners, listener = listener }; 341 | variables.listeners = head; 342 | return this; 343 | } 344 | 345 | 346 | // set the parent bean factory 347 | public any function setParent( any parent ) { 348 | variables.parent = parent; 349 | return this; 350 | } 351 | 352 | // PRIVATE METHODS 353 | 354 | private boolean function beanIsTransient( string singleDir, string dir, string beanName ) { 355 | return singleDir == 'bean' || 356 | structKeyExists( variables.transients, dir ) || 357 | ( structKeyExists( variables.config, "singletonPattern" ) && 358 | refindNoCase( variables.config.singletonPattern, beanName ) == 0 ) || 359 | ( structKeyExists( variables.config, "transientPattern" ) && 360 | refindNoCase( variables.config.transientPattern, beanName ) > 0 ); 361 | } 362 | 363 | 364 | private any function cachable( string beanName) { 365 | var newObject = false; 366 | var info = variables.beanInfo[ beanName ]; 367 | if ( info.isSingleton ) { 368 | // cache on the qualified bean name: 369 | var qualifiedName = beanName; 370 | if ( structKeyExists( info, 'name' ) && structKeyExists( info, 'qualifier' ) ) { 371 | qualifiedName = info.name & info.qualifier; 372 | } 373 | if ( !structKeyExists( variables.beanCache, qualifiedName ) ) { 374 | variables.beanCache[ qualifiedName ] = construct( info.cfc ); 375 | newObject = true; 376 | } 377 | return { bean = variables.beanCache[ qualifiedName ], newObject = newObject }; 378 | } else { 379 | return { bean = construct( info.cfc ), newObject = true }; 380 | } 381 | } 382 | 383 | 384 | private struct function cleanMetadata( string cfc ) { 385 | var baseMetadata = metadata( cfc ); 386 | var iocMeta = { setters = { }, pruned = false, type = baseMetadata.type }; 387 | var md = { extends = baseMetadata }; 388 | do { 389 | md = md.extends; 390 | // gather up setters based on metadata: 391 | var implicitSetters = false; 392 | // we have implicit setters if: accessors="true" or persistent="true" 393 | if ( structKeyExists( md, 'persistent' ) && isBoolean( md.persistent ) ) { 394 | implicitSetters = md.persistent; 395 | } 396 | if ( structKeyExists( md, 'accessors' ) && isBoolean( md.accessors ) ) { 397 | implicitSetters = implicitSetters || md.accessors; 398 | } 399 | if ( structKeyExists( md, 'properties' ) ) { 400 | // due to a bug in ACF9.0.1, we cannot use var property in md.properties, 401 | // instead we must use an explicit loop index... ugh! 402 | var n = arrayLen( md.properties ); 403 | for ( var i = 1; i <= n; ++i ) { 404 | var property = md.properties[ i ]; 405 | if ( implicitSetters && 406 | ( !structKeyExists( property, 'setter' ) || 407 | isBoolean( property.setter ) && property.setter ) ) { 408 | if ( structKeyExists( property, 'type' ) && 409 | property.type != 'any' && 410 | variables.config.omitTypedProperties ) { 411 | iocMeta.setters[ property.name ] = 'ignored'; 412 | } else if ( structKeyExists( property, 'default' ) && 413 | variables.config.omitDefaultedProperties ) { 414 | iocMeta.setters[ property.name ] = 'ignored'; 415 | } else { 416 | iocMeta.setters[ property.name ] = 'implicit'; 417 | } 418 | } 419 | } 420 | } 421 | // still looking for a constructor? 422 | if ( !structKeyExists( iocMeta, 'constructor' ) ) { 423 | if ( structKeyExists( md, 'functions' ) ) { 424 | // due to a bug in ACF9.0.1, we cannot use var property in md.functions, 425 | // instead we must use an explicit loop index... ugh! 426 | var n = arrayLen( md.functions ); 427 | for ( var i = 1; i <= n; ++i ) { 428 | var func = md.functions[ i ]; 429 | if ( func.name == 'init' ) { 430 | iocMeta.constructor = { }; 431 | if ( structKeyExists( func, 'parameters' ) ) { 432 | // due to a bug in ACF9.0.1, we cannot use var arg in func.parameters, 433 | // instead we must use an explicit loop index... ugh! 434 | var m = arrayLen( func.parameters ); 435 | for ( var j = 1; j <= m; ++j ) { 436 | var arg = func.parameters[ j ]; 437 | iocMeta.constructor[ arg.name ] = structKeyExists( arg, 'required' ) ? arg.required : false; 438 | } 439 | } 440 | } 441 | } 442 | } 443 | } 444 | } while ( structKeyExists( md, 'extends' ) ); 445 | return iocMeta; 446 | } 447 | 448 | 449 | // in case an extension point wants to override actual construction: 450 | private any function construct( string dottedPath ) { 451 | return createObject( 'component', dottedPath ); 452 | } 453 | 454 | 455 | // in case an extension point wants to override actual metadata retrieval: 456 | private any function metadata( string dottedPath ) { 457 | try { 458 | return getComponentMetadata( dottedPath ); 459 | } catch ( any e ) { 460 | var except = "Unable to getComponentMetadata(#dottedPath#) because: " & 461 | e.message & ( len( e.detail ) ? " (#e.detail#)" : "" ); 462 | try { 463 | except = except & ", near line " & e.tagContext[1].line & 464 | " in " & e.tagContext[1].template; 465 | } catch ( any e ) { 466 | // unable to determine template / line number so just use 467 | // the exception message we built so far 468 | } 469 | throw except; 470 | } 471 | } 472 | 473 | 474 | private string function deduceDottedPath( string baseMapping, string basePath ) { 475 | if ( right( basePath, 1 ) == '/' && len( basePath ) > 1 ) { 476 | basePath = left( basePath, len( basePath ) - 1 ); 477 | } 478 | var cfcPath = left( baseMapping, 1 ) == '/' ? 479 | ( len( baseMapping ) > 1 ? right( baseMapping, len( baseMapping ) - 1 ) : '' ) : 480 | getFileFromPath( baseMapping ); 481 | if ( right( cfcPath, 1 ) == '/' && len( cfcPath ) > 1 ) { 482 | cfcPath = left( cfcPath, len( cfcPath ) - 1 ); 483 | } 484 | var expPath = basePath; 485 | var notFound = true; 486 | var dotted = ''; 487 | do { 488 | var mapped = cfcPath; 489 | if ( len( mapped ) && left( mapped, 1 ) != '.' ) mapped = '/' & mapped; 490 | var mappedPath = replace( expandpath( mapped ), chr(92), '/', 'all' ); 491 | if ( mappedPath == basePath ) { 492 | dotted = replace( cfcPath, '/', '.', 'all' ); 493 | notFound = false; 494 | break; 495 | } 496 | var prevPath = expPath; 497 | expPath = replace( getDirectoryFromPath( expPath ), chr(92), '/', 'all' ); 498 | if ( right( expPath, 1 ) == '/' && len( expPath ) > 1 ) { 499 | expPath = left( expPath, len( expPath ) - 1 ); 500 | } 501 | var progress = prevPath != expPath; 502 | var piece = listLast( expPath, '/' ); 503 | cfcPath = piece & '/' & cfcPath; 504 | } while ( progress ); 505 | if ( notFound ) { 506 | throw 'unable to deduce dot-relative path for: #baseMapping# (#basePath#) root #expandPath("/")#'; 507 | } 508 | return dotted; 509 | } 510 | 511 | 512 | private void function discoverBeans() { 513 | if ( structKeyExists( variables, 'discoveryComplete' ) ) return; 514 | lock name="#application.applicationName#_ioc1_#variables.folderList#" type="exclusive" timeout="30" { 515 | if ( structKeyExists( variables, 'discoveryComplete' ) ) return; 516 | variables.pathMapCache = { }; 517 | try { 518 | for ( var f in variables.folderArray ) { 519 | discoverBeansInFolder( replace( f, chr(92), '/', 'all' ) ); 520 | } 521 | } finally { 522 | variables.discoveryComplete = true; 523 | } 524 | } 525 | onLoadEvent(); 526 | } 527 | 528 | 529 | private void function discoverBeansInFolder( string mapping ) { 530 | var folder = replace( expandPath( mapping ), chr(92), '/', 'all' ); 531 | var dotted = deduceDottedPath( mapping, folder ); 532 | var cfcs = [ ]; 533 | try { 534 | cfcs = directoryList( folder, variables.config.recurse, 'path', '*.cfc' ); 535 | } catch ( any e ) { 536 | // assume bad path - ignore it, cfcs is empty list 537 | } 538 | local.beansWithDuplicates = ""; 539 | for ( var cfcOSPath in cfcs ) { 540 | var cfcPath = replace( cfcOSPath, chr(92), '/', 'all' ); 541 | // watch out for excluded paths: 542 | var excludePath = false; 543 | for ( var pattern in variables.config.exclude ) { 544 | if ( findNoCase( pattern, cfcPath ) ) { 545 | excludePath = true; 546 | break; 547 | } 548 | } 549 | if ( excludePath ) continue; 550 | var relPath = right( cfcPath, len( cfcPath ) - len( folder ) ); 551 | var extN = 1 + len( listLast( cfcPath, "." ) ); 552 | relPath = left( relPath, len( relPath ) - extN ); 553 | var dir = listLast( getDirectoryFromPath( cfcPath ), '/' ); 554 | var singleDir = singular( dir ); 555 | var beanName = listLast( relPath, '/' ); 556 | var dottedPath = dotted & replace( relPath, '/', '.', 'all' ); 557 | try { 558 | var metadata = { 559 | name = beanName, qualifier = singleDir, isSingleton = !beanIsTransient( singleDir, dir, beanName ), 560 | path = cfcPath, cfc = dottedPath, metadata = cleanMetadata( dottedPath ) 561 | }; 562 | if ( structKeyExists( metadata.metadata, "type" ) && metadata.metadata.type == "interface" ) { 563 | continue; 564 | } 565 | 566 | if ( variables.config.omitDirectoryAliases ) { 567 | if ( structKeyExists( variables.beanInfo, beanName ) ) { 568 | throw '#beanName# is not unique'; 569 | } 570 | variables.beanInfo[ beanName ] = metadata; 571 | } else { 572 | if ( listFindNoCase(local.beansWithDuplicates, beanName) ) {} 573 | else if ( structKeyExists( variables.beanInfo, beanName ) ) { 574 | structDelete( variables.beanInfo, beanName ); 575 | local.beansWithDuplicates = listAppend(local.beansWithDuplicates, beanName); 576 | } else { 577 | variables.beanInfo[ beanName ] = metadata; 578 | } 579 | if ( structKeyExists( variables.beanInfo, beanName & singleDir ) ) { 580 | throw '#beanName & singleDir# is not unique'; 581 | } 582 | variables.beanInfo[ beanName & singleDir ] = metadata; 583 | } 584 | 585 | } catch ( any e ) { 586 | // wrap the exception so we can add bean name for debugging 587 | // this trades off any stack trace information for the bean name but 588 | // since we are only trying to get metadata, the latter should be 589 | // more useful than the former 590 | var except = "Problem with metadata for #beanName# (#dottedPath#) because: " & 591 | e.message & ( len( e.detail ) ? " (#e.detail#)" : "" ); 592 | throw except; 593 | } 594 | } 595 | } 596 | 597 | 598 | private struct function findSetters( any cfc, struct iocMeta ) { 599 | var liveMeta = { setters = iocMeta.setters }; 600 | if ( !iocMeta.pruned ) { 601 | // need to prune known setters of transients: 602 | var prunable = { }; 603 | for ( var known in iocMeta.setters ) { 604 | if ( !isSingleton( known ) ) { 605 | prunable[ known ] = true; 606 | } 607 | } 608 | for ( known in prunable ) { 609 | structDelete( iocMeta.setters, known ); 610 | } 611 | iocMeta.pruned = true; 612 | } 613 | // gather up explicit setters: 614 | for ( var member in cfc ) { 615 | var method = cfc[ member ]; 616 | var n = len( member ); 617 | if ( isCustomFunction( method ) && left( member, 3 ) == 'set' && n > 3 ) { 618 | var property = right( member, n - 3 ); 619 | if ( !isSingleton( property ) ) { 620 | // ignore properties that we know to be transients... 621 | continue; 622 | } 623 | if ( !structKeyExists( liveMeta.setters, property ) ) { 624 | liveMeta.setters[ property ] = 'explicit'; 625 | } 626 | } 627 | } 628 | return liveMeta; 629 | } 630 | 631 | 632 | private any function forceCache( any bean, string beanName) { 633 | var info = variables.beanInfo[ beanName ]; 634 | if ( info.isSingleton ) { 635 | // cache on the qualified bean name: 636 | var qualifiedName = beanName; 637 | if ( structKeyExists( info, 'name' ) && structKeyExists( info, 'qualifier' ) ) { 638 | qualifiedName = info.name & info.qualifier; 639 | } 640 | variables.beanCache[ qualifiedName ] = bean; 641 | } 642 | } 643 | 644 | 645 | private boolean function isConstant ( string beanName ) { 646 | return structKeyExists( variables.beanInfo, beanName ) && 647 | structKeyExists( variables.beanInfo[ beanName ], 'value' ); 648 | } 649 | 650 | 651 | private void function logMissingBean( string beanName, string resolvingBeanName = '' ) { 652 | var sys = createObject( 'java', 'java.lang.System' ); 653 | if ( len( resolvingBeanName ) ) { 654 | sys.out.println( 'bean not found: #beanName#; while resolving #resolvingBeanName#' ); 655 | } else { 656 | sys.out.println( 'bean not found: #beanName#' ); 657 | } 658 | } 659 | 660 | 661 | /* 662 | * override this if you want to add a convention-based bean factory hook, that returns 663 | * beans instead of throwing an exception 664 | */ 665 | private any function missingBean( string beanName, string resolvingBeanName = '', boolean dependency = true ) { 666 | if ( variables.config.strict || !dependency ) { 667 | if ( len( resolvingBeanName ) ) { 668 | throw 'bean not found: #beanName#; while resolving #resolvingBeanName#'; 669 | } else { 670 | throw 'bean not found: #beanName#'; 671 | } 672 | } else { 673 | logMissingBean( beanName, resolvingBeanName ); 674 | } 675 | } 676 | 677 | 678 | private void function onLoadEvent() { 679 | var head = variables.listeners; 680 | while ( isStruct( head ) ) { 681 | if ( isCustomFunction( head.listener ) || isClosure( head.listener ) ) { 682 | head.listener( this ); 683 | } else if ( isObject( head.listener ) ) { 684 | head.listener.onLoad( this ); 685 | } else if ( isSimpleValue( head.listener ) && 686 | containsBean( head.listener ) ) { 687 | getBean( head.listener ).onLoad( this ); 688 | } else { 689 | throw "invalid onLoad listener registered: #head.listener.toString()#"; 690 | } 691 | head = head.next; 692 | } 693 | } 694 | 695 | 696 | private any function parentBeanInfo( string beanName ) { 697 | // intended to be adaptable to whatever the parent is: 698 | if ( structKeyExists( variables.parent, 'getBeanInfo' ) ) { 699 | // smells like DI/1 or compatible: 700 | return variables.parent.getBeanInfo( beanName ); 701 | } 702 | if ( structKeyExists( variables.parent, 'getBeanDefinition' ) ) { 703 | // smells like ColdSpring or compatible: 704 | return variables.parent.getBeanDefinition( beanName ); 705 | } 706 | // unknown: 707 | return { }; 708 | } 709 | 710 | 711 | private any function parentBeanInfoList( boolean flatten ) { 712 | // intended to be adaptable to whatever the parent is: 713 | if ( structKeyExists( variables.parent, 'getBeanInfo' ) ) { 714 | // smells like DI/1 or compatible: 715 | return variables.parent.getBeanInfo( flatten = flatten ); 716 | } 717 | if ( structKeyExists( variables.parent, 'getBeanDefinitionList' ) ) { 718 | // smells like ColdSpring or compatible: 719 | return variables.parent.getBeanDefinitionList(); 720 | } 721 | // unknown 722 | return { }; 723 | } 724 | 725 | 726 | private any function resolveBean( string beanName, struct constructorArgs = { } ) { 727 | // do enough resolution to create and initialization this bean 728 | // returns a struct of the bean and a struct of beans and setters still to run 729 | // construction phase: 730 | var accumulator = { injection = { } }; 731 | // ensure only injection singletons end up cached in variables scope 732 | if ( structKeyExists( variables.accumulatorCache, beanName ) ) { 733 | structAppend( accumulator.injection, variables.accumulatorCache[ beanName ].injection ); 734 | } else { 735 | variables.accumulatorCache[ beanName ] = { injection = { }, dependencies = { } }; 736 | } 737 | // all dependencies can be cached in variables scope 738 | accumulator.dependencies = variables.accumulatorCache[ beanName ].dependencies; 739 | var partialBean = resolveBeanCreate( beanName, accumulator, constructorArgs ); 740 | if ( structKeyExists( variables.resolutionCache, beanName ) && 741 | variables.resolutionCache[ beanName ] ) { 742 | // fully resolved, no action needed this time 743 | } else { 744 | var checkForPostInjection = structKeyExists( variables.config, 'initMethod' ); 745 | var initMethod = checkForPostInjection ? variables.config.initMethod : ''; 746 | var postInjectables = { }; 747 | // injection phase: 748 | // now perform all of the injection: 749 | for ( var name in partialBean.injection ) { 750 | if ( structKeyExists( variables.accumulatorCache[ beanName ].injection, name ) ) { 751 | // this singleton is in the accumulatorCache, thus it has already been fully resolved 752 | continue; 753 | } 754 | var injection = partialBean.injection[ name ]; 755 | if ( checkForPostInjection && !isConstant( name ) && structKeyExists( injection.bean, initMethod ) ) { 756 | postInjectables[ name ] = true; 757 | } 758 | for ( var property in injection.setters ) { 759 | if ( injection.setters[ property ] == 'ignored' ) { 760 | // do not inject defaulted/typed properties! 761 | continue; 762 | } 763 | var args = { }; 764 | if ( structKeyExists( injection.overrides, property ) ) { 765 | args[ property ] = injection.overrides[ property ]; 766 | } else if ( structKeyExists( partialBean.injection, property ) ) { 767 | args[ property ] = partialBean.injection[ property ].bean; 768 | } else if ( hasParent() && variables.parent.containsBean( property ) ) { 769 | args[ property ] = variables.parent.getBean( property ); 770 | } else { 771 | // allow for possible convention-based bean factory 772 | args[ property ] = missingBean( property, beanName ); 773 | // isNull() does not always work on ACF10... 774 | try { if ( isNull( args[ property ] ) ) continue; } catch ( any e ) { continue; } 775 | } 776 | invoke( injection.bean, "set#property#", args ); 777 | } 778 | } 779 | // post-injection, pre-init-method phase: 780 | for ( name in partialBean.injection ) { 781 | injection = partialBean.injection[ name ]; 782 | setupInitMethod( name, injection.bean ); 783 | } 784 | // see if anything needs post-injection, init-method calls: 785 | for ( var postName in postInjectables ) { 786 | callInitMethod( postName, postInjectables, partialBean, initMethod ); 787 | } 788 | for ( name in partialBean.injection ) { 789 | if ( isSingleton( name ) ) { 790 | variables.accumulatorCache[ beanName ].injection[ name ] = partialBean.injection[ name ]; 791 | } 792 | } 793 | variables.resolutionCache[ beanName ] = isSingleton( beanName ); 794 | } 795 | return partialBean.bean; 796 | } 797 | 798 | 799 | private void function callInitMethod( string name, struct injectables, struct info, string method ) { 800 | 801 | if ( injectables[ name ] ) { 802 | injectables[ name ] = false; // this ensures we don't try to init the same 803 | // bean twice - and also breaks circular dependencies... 804 | if ( structKeyExists( info.dependencies, name ) ) { 805 | for ( var depName in info.dependencies[ name ] ) { 806 | if ( structKeyExists( injectables, depName ) && 807 | injectables[ depName ] ) { 808 | callInitMethod( depName, injectables, info, method ); 809 | } 810 | } 811 | } 812 | if ( structKeyExists( variables.initMethodCache, name ) && 813 | variables.initMethodCache[ name ] ) { 814 | } else { 815 | variables.initMethodCache[ name ] = isSingleton( name ); 816 | var bean = info.injection[ name ].bean; 817 | invoke( bean, method ); 818 | } 819 | } 820 | } 821 | 822 | 823 | private struct function resolveBeanCreate( string beanName, struct accumulator, struct constructorArgs = { } ) { 824 | var bean = 0; 825 | if ( structKeyExists( variables.beanInfo, beanName ) ) { 826 | var info = variables.beanInfo[ beanName ]; 827 | if ( !structKeyExists( accumulator.dependencies, beanName ) ) accumulator.dependencies[ beanName ] = { }; 828 | if ( structKeyExists( info, 'cfc' ) ) { 829 | /*******************************************************/ 830 | var metaBean = cachable( beanName ); 831 | var overrides = { }; 832 | // be careful not to modify overrides metadata: 833 | if ( structCount( constructorArgs ) ) { 834 | if ( structKeyExists( info, 'overrides' ) ) { 835 | structAppend( overrides, info.overrides ); 836 | } 837 | structAppend( overrides, constructorArgs ); 838 | } else { 839 | if ( structKeyExists( info, 'overrides' ) ) { 840 | overrides = info.overrides; 841 | } 842 | } 843 | bean = metaBean.bean; 844 | if ( metaBean.newObject ) { 845 | if ( structKeyExists( info.metadata, 'constructor' ) ) { 846 | var args = { }; 847 | for ( var arg in info.metadata.constructor ) { 848 | accumulator.dependencies[ beanName ][ arg ] = true; 849 | var argBean = { }; 850 | // handle known required arguments 851 | if ( info.metadata.constructor[ arg ] ) { 852 | var beanMissing = true; 853 | if ( structKeyExists( overrides, arg ) ) { 854 | args[ arg ] = overrides[ arg ]; 855 | beanMissing = false; 856 | } else if ( containsBean( arg ) ) { 857 | argBean = resolveBeanCreate( arg, accumulator ); 858 | if ( structKeyExists( argBean, 'bean' ) ) { 859 | args[ arg ] = argBean.bean; 860 | beanMissing = false; 861 | } 862 | } 863 | if ( beanMissing ) { 864 | throw 'bean not found: #arg#; while resolving constructor arguments for #beanName#'; 865 | } 866 | } else { 867 | if ( structKeyExists( overrides, arg ) ) { 868 | args[ arg ] = overrides[ arg ]; 869 | } else if ( containsBean( arg ) ) { 870 | // optional but present 871 | argBean = resolveBeanCreate( arg, accumulator ); 872 | if ( structKeyExists( argBean, 'bean' ) ) { 873 | args[ arg ] = argBean.bean; 874 | } 875 | } else { 876 | // optional but not present 877 | } 878 | } 879 | } 880 | var __ioc_newBean = bean.init( argumentCollection = args ); 881 | // if the constructor returns anything, it becomes the bean 882 | // this allows for smart constructors that return things other 883 | // than the CFC being created, such as implicit factory beans 884 | // and automatic singletons etc (rare practices in CFML but...) 885 | if ( !isNull( __ioc_newBean ) ) { 886 | bean = __ioc_newBean; 887 | forceCache( bean, beanName ); 888 | } 889 | } 890 | } 891 | /*******************************************************/ 892 | if ( !structKeyExists( accumulator.injection, beanName ) ) { 893 | if ( !structKeyExists( variables.settersInfo, beanName ) ) { 894 | variables.settersInfo[ beanName ] = findSetters( bean, info.metadata ); 895 | } 896 | var setterMeta = { 897 | setters = variables.settersInfo[ beanName ].setters, 898 | bean = bean, 899 | overrides = overrides 900 | }; 901 | accumulator.injection[ beanName ] = setterMeta; 902 | for ( var property in setterMeta.setters ) { 903 | accumulator.dependencies[ beanName ][ property ] = true; 904 | if ( structKeyExists( overrides, property ) ) { 905 | // skip resolution because we'll inject override 906 | } else { 907 | resolveBeanCreate( property, accumulator ); 908 | } 909 | } 910 | } 911 | } else if ( isConstant( beanName ) ) { 912 | bean = info.value; 913 | accumulator.injection[ beanName ] = { bean = info.value, setters = { } }; 914 | } else if ( structKeyExists( info, 'factory' ) ) { 915 | var fmBean = isSimpleValue( info.factory ) ? this.getBean( info.factory ) : info.factory; 916 | var nArgs = arrayLen( info.args ); 917 | var argStruct = { }; 918 | for ( var i = 1; i <= nArgs; ++i ) { 919 | var argName = info.args[ i ]; 920 | if ( structKeyExists( info.overrides, argName ) ) { 921 | argStruct[ i ] = info.overrides[ argName ]; 922 | } else { 923 | argStruct[ i ] = this.getBean( argName ); 924 | } 925 | } 926 | if ( isCustomFunction( fmBean ) || isClosure( fmBean ) ) { 927 | bean = fmBean( argumentCollection = argStruct ); 928 | } else { 929 | bean = invoke( fmBean, "#info.method#", argStruct ); 930 | } 931 | accumulator.injection[ beanName ] = { bean = bean, setters = { } }; 932 | } else { 933 | throw 'internal error: invalid metadata for #beanName#'; 934 | } 935 | } else { 936 | if ( hasParent() && variables.parent.containsBean( beanName ) ) { 937 | bean = variables.parent.getBean( beanName ); 938 | } else { 939 | bean = missingBean( beanName = beanName, dependency = true ); 940 | } 941 | if ( !isNull( bean ) ) { 942 | accumulator.injection[ beanName ] = { bean = bean, setters = { } }; 943 | } 944 | } 945 | return { 946 | bean = bean, 947 | injection = accumulator.injection, 948 | dependencies = accumulator.dependencies 949 | }; 950 | } 951 | 952 | 953 | private void function setupFrameworkDefaults() { 954 | param name = "variables.config.recurse" default = true; 955 | param name = "variables.config.strict" default = false; 956 | 957 | if ( !structKeyExists( variables.config, 'exclude' ) ) { 958 | variables.config.exclude = [ ]; 959 | } 960 | for ( var elem in variables.autoExclude ) { 961 | arrayAppend( variables.config.exclude, replace( elem, chr(92), '/', 'all' ) ); 962 | } 963 | 964 | // install bean factory constant: 965 | variables.beanInfo.beanFactory = { value = this, isSingleton = true }; 966 | if ( structKeyExists( variables.config, 'constants' ) ) { 967 | for ( var beanName in variables.config.constants ) { 968 | variables.beanInfo[ beanName ] = { value = variables.config.constants[ beanName ], isSingleton = true }; 969 | } 970 | } 971 | 972 | variables.transients = { }; 973 | if ( structKeyExists( variables.config, 'transients' ) ) { 974 | for ( var transientFolder in variables.config.transients ) { 975 | variables.transients[ transientFolder ] = true; 976 | } 977 | } 978 | 979 | if ( structKeyExists( variables.config, 'singletonPattern' ) && 980 | structKeyExists( variables.config, 'transientPattern' ) ) { 981 | throw 'singletonPattern and transientPattern are mutually exclusive'; 982 | } 983 | 984 | if ( !structKeyExists( variables.config, 'omitDefaultedProperties' ) ) { 985 | variables.config.omitDefaultedProperties = true; 986 | } 987 | if ( !structKeyExists( variables.config, 'omitTypedProperties' ) ) { 988 | variables.config.omitTypedProperties = true; 989 | } 990 | if ( !structKeyExists( variables.config, 'omitDirectoryAliases' ) ) { 991 | variables.config.omitDirectoryAliases = false; 992 | } 993 | if ( !structKeyExists( variables.config, 'singulars' ) ) { 994 | variables.config.singulars = { }; 995 | } 996 | if ( !structKeyExists( variables.config, 'liberal' ) ) { 997 | variables.config.liberal = false; 998 | } 999 | 1000 | variables.config.version = variables._di1_version; 1001 | } 1002 | 1003 | 1004 | // hook for extension points to process beans after they have been 1005 | // constructed and injected, but before init-method is called on anything 1006 | private void function setupInitMethod( string name, any bean ) { 1007 | } 1008 | 1009 | 1010 | private string function singular( string plural ) { 1011 | if ( structKeyExists( variables.config.singulars, plural ) ) { 1012 | return variables.config.singulars[ plural ]; 1013 | } 1014 | var single = plural; 1015 | var n = len( plural ); 1016 | var last = right( plural, 1 ); 1017 | if ( last == 's' ) { 1018 | if ( variables.config.liberal && n > 3 && right( plural, 3 ) == 'ies' ) { 1019 | single = left( plural, n - 3 ) & 'y'; 1020 | } else { 1021 | single = left( plural, n - 1 ); 1022 | } 1023 | } 1024 | return single; 1025 | } 1026 | 1027 | } 1028 | --------------------------------------------------------------------------------