diff --git a/.gitignore b/.gitignore index c5744476..c835999b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,10 @@ WEB-INF/* litepost/* +mxunit/* .project settings.xml -.settings/org.eclipse.core.resources.prefs \ No newline at end of file +.settings/org.eclipse.core.resources.prefs +# Intellij IDEA files +*.iml +*.ipr +*.idea \ No newline at end of file diff --git a/README.txt b/README.txt index 2f877366..19dcf625 100644 --- a/README.txt +++ b/README.txt @@ -1,7 +1,8 @@ -This FW/1 directory is a complete web application and expects to live in its own webroot -if you plan to run the applications within it. To use FW/1 in a separate webroot you can -either copy the org directory to that webroot or add a mapping for /org/corfield to the -corfield folder inside the org directory (or a /org mapping to the org directory directly). +This FW/1 directory is a complete web application and expects to live in its own +webroot if you plan to run the applications within it. To use FW/1 in a separate +webroot you can either copy the org directory to that webroot or add a mapping +for /org/corfield to the corfield folder inside the org directory (or a /org +mapping to the org directory directly). Project home: http://fw1.riaforge.org @@ -9,4 +10,4 @@ Documentation wiki: http://github.com/seancorfield/fw1/wiki Blog: http://corfield.org/blog/archives.cfm/category/fw1 -Support: http://groups.google.com/group/framework-one/ \ No newline at end of file +Support: http://groups.google.com/group/framework-one/ diff --git a/docs/fw1.pdf b/docs/fw1.pdf new file mode 100644 index 00000000..a560f946 Binary files /dev/null and b/docs/fw1.pdf differ diff --git a/examples/Application.cfc b/examples/Application.cfc index 9ab828e6..3aaf145f 100644 --- a/examples/Application.cfc +++ b/examples/Application.cfc @@ -6,7 +6,8 @@ component extends="org.corfield.framework" { // FW/1 - configuration: variables.framework = { usingSubsystems = true, - SESOmitIndex = true + SESOmitIndex = true, + trace = true }; // pull in bean factory for hello8: @@ -17,4 +18,4 @@ component extends="org.corfield.framework" { } } -} \ No newline at end of file +} diff --git a/examples/hello8/model/ioc.cfc b/examples/hello8/model/ioc.cfc index 1ae53be5..f36adc20 100644 --- a/examples/hello8/model/ioc.cfc +++ b/examples/hello8/model/ioc.cfc @@ -1,5 +1,5 @@ /* - Copyright (c) 2010-2011, Sean Corfield + Copyright (c) 2010-2012, Sean Corfield Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -23,15 +23,26 @@ component { variables.beanInfo = { }; variables.beanCache = { }; variables.autoExclude = [ '/WEB-INF', '/Application.cfc' ]; + variables.listeners = 0; setupFrameworkDefaults(); return this; } // PUBLIC METHODS + + // programmatically register an alias + public any function addAlias( string aliasName, string beanName ) { + discoverBeans( variables.folders ); + variables.beanInfo[ aliasName ] = variables.beanInfo[ beanName ]; + return this; + } + // programmatically register new beans with the factory (add a singleton name/value pair) - public void function addBean( string beanName, any beanValue ) { + public any function addBean( string beanName, any beanValue ) { + discoverBeans( variables.folders ); variables.beanInfo[ beanName ] = { value = beanValue, isSingleton = true }; + return this; } @@ -44,19 +55,21 @@ component { // programmatically register new beans with the factory (add an actual CFC) - public void function declareBean( string beanName, string dottedPath, boolean isSingleton = true ) { + public any function declareBean( string beanName, string dottedPath, boolean isSingleton = true ) { + discoverBeans( variables.folders ); var singleDir = ''; if ( listLen( dottedPath, '.' ) > 1 ) { var cfc = listLast( dottedPath, '.' ); var dottedPart = left( dottedPath, len( dottedPath ) - len( cfc ) - 1 ); singleDir = singular( listLast( dottedPart, '.' ) ); } - var cfcPath = replace( expandPath( '/' & replace( dottedPath, '.', '/', 'all' ) & '.cfc' ), '\', '/', 'all' ); + var cfcPath = replace( expandPath( '/' & replace( dottedPath, '.', '/', 'all' ) & '.cfc' ), chr(92), '/', 'all' ); var metadata = { name = beanName, qualifier = singleDir, isSingleton = isSingleton, path = cfcPath, cfc = dottedPath, metadata = cleanMetadata( dottedPath ) }; variables.beanInfo[ beanName ] = metadata; + return this; } @@ -64,20 +77,7 @@ component { public any function getBean( string beanName ) { discoverBeans( variables.folders ); if ( structKeyExists( variables.beanInfo, beanName ) ) { - var info = variables.beanInfo[ beanName ]; - if ( info.isSingleton ) { - // cache on the qualified bean name: - var qualifiedName = beanName; - if ( structKeyExists( info, 'name' ) && structKeyExists( info, 'qualifier' ) ) { - qualifiedName = info.name & info.qualifier; - } - if ( !structKeyExists( variables.beanCache, qualifiedName ) ) { - variables.beanCache[ qualifiedName ] = resolveBean( beanName ); - } - return variables.beanCache[ qualifiedName ]; - } else { - return resolveBean( beanName ); - } + return resolveBean( beanName ); } else if ( structKeyExists( variables, 'parent' ) ) { return variables.parent.getBean( beanName ); } else { @@ -87,6 +87,7 @@ component { // convenience API for metaprogramming perhaps? public any function getBeanInfo( string beanName = '' ) { + discoverBeans( variables.folders ); if ( len( beanName ) ) { if ( structKeyExists( variables.beanInfo, beanName ) ) { return variables.beanInfo[ beanName ]; @@ -105,6 +106,7 @@ component { // return true iff bean is known to be a singleton public boolean function isSingleton( string beanName ) { + discoverBeans( variables.folders ); if ( structKeyExists( variables.beanInfo, beanName ) ) { return variables.beanInfo[ beanName ].isSingleton; } else if ( structKeyExists( variables, 'parent' ) ) { @@ -115,9 +117,13 @@ component { } - // given a bean (by name or by value), call the named setters with the specified property values + // given a bean (by name, by type or by value), call the named + // setters with the specified property values public any function injectProperties( any bean, struct properties ) { - if ( !isSimpleValue( bean ) ) bean = getBean( bean ); + if ( isSimpleValue( bean ) ) { + if ( containsBean( bean ) ) bean = getBean( bean ); + else bean = createObject( 'component', bean ); + } for ( var property in properties ) { var args = { }; args[ property ] = properties[ property ]; @@ -132,26 +138,57 @@ component { // are responsible for dealing with that logic (it's safe to reload a child but // if you reload the parent, you must reload *all* child factories to ensure // things stay consistent!) - public void function load() { + public any function load() { discoverBeans( variables.folders ); variables.beanCache = { }; for ( var key in variables.beanInfo ) { if ( variables.beanInfo[ key ].isSingleton ) getBean( key ); } + return this; } + + + // add a listener for processing after a (re)load of the factory + // called with just the factory, should be a plain function + public any function onLoad( any listener ) { + var head = { next = variables.listeners, listener = listener }; + variables.listeners = head; + return this; + } // set the parent bean factory - public void function setParent( any parent ) { + public any function setParent( any parent ) { variables.parent = parent; + return this; } // PRIVATE METHODS private boolean function beanIsTransient( string singleDir, string dir, string beanName ) { - return singleDir == 'bean' || structKeyExists( variables.transients, dir ); + return singleDir == 'bean' || structKeyExists( variables.transients, dir ) || ( structKeyExists( variables.config, "singletonPattern" ) && refindNoCase( variables.config.singletonPattern, beanName ) == 0 ); } - + + + private any function cachable( string beanName) { + var newObject = false; + var info = variables.beanInfo[ beanName ]; + if ( info.isSingleton ) { + // cache on the qualified bean name: + var qualifiedName = beanName; + if ( structKeyExists( info, 'name' ) && structKeyExists( info, 'qualifier' ) ) { + qualifiedName = info.name & info.qualifier; + } + if ( !structKeyExists( variables.beanCache, qualifiedName ) ) { + variables.beanCache[ qualifiedName ] = createObject( 'component', info.cfc ); + newObject = true; + } + return { bean = variables.beanCache[ qualifiedName ], newObject = newObject }; + } else { + return { bean = createObject( 'component', info.cfc ), newObject = true }; + } + } + private struct function cleanMetadata( string cfc ) { var baseMetadata = getComponentMetadata( cfc ); @@ -159,8 +196,8 @@ component { var md = { extends = baseMetadata }; do { md = md.extends; - // gather up setters based on metadata: - var implicitSetters = false; + // gather up setters based on metadata: + var implicitSetters = false; // we have implicit setters if: accessors="true" or persistent="true" if ( structKeyExists( md, 'persistent' ) && isBoolean( md.persistent ) ) { implicitSetters = md.persistent; @@ -176,10 +213,6 @@ component { var property = md.properties[ i ]; if ( implicitSetters || structKeyExists( property, 'setter' ) && isBoolean( property.setter ) && property.setter ) { - if ( !isSingleton( property.name ) ) { - // ignore properties that we know to be transients... - continue; - } iocMeta.setters[ property.name ] = 'implicit'; } } @@ -200,7 +233,7 @@ component { var m = arrayLen( func.parameters ); for ( var j = 1; j <= m; ++j ) { var arg = func.parameters[ j ]; - iocMeta.constructor[ arg.name ] = structKeyExists( arg, 'type' ) ? arg.type : 'any'; + iocMeta.constructor[ arg.name ] = structKeyExists( arg, 'required' ) ? arg.required : false; } } } @@ -225,7 +258,7 @@ component { return remaining; } } else { - var webroot = replace( expandPath( '/' ), '\', '/', 'all' ); + var webroot = replace( expandPath( '/' ), chr(92), '/', 'all' ); if ( path.startsWith( webroot ) ) { var rootRelativePath = right( path, len( path ) - len( webroot ) ); return replace( left( rootRelativePath, len( rootRelativePath ) - 4 ), '/', '.', 'all' ); @@ -243,16 +276,17 @@ component { var folderArray = listToArray( folders ); variables.pathMapCache = { }; for ( var f in folderArray ) { - discoverBeansInFolder( replace( trim( f ), '\', '/', 'all' ) ); + discoverBeansInFolder( replace( trim( f ), chr(92), '/', 'all' ) ); } variables.discoveryComplete = true; } + onLoadEvent(); } private void function discoverBeansInFolder( string mapping ) { - var folder = replace( expandPath( mapping ), '\', '/', 'all' ); - var webroot = replace( expandPath( '/' ), '\', '/', 'all' ); + var folder = replace( expandPath( mapping ), chr(92), '/', 'all' ); + var webroot = replace( expandPath( '/' ), chr(92), '/', 'all' ); if ( mapping.startsWith( webroot ) ) { // must be an already expanded path! folder = mapping; @@ -270,7 +304,7 @@ component { // find all the CFCs here: var cfcs = directoryList( folder, variables.config.recurse, 'path', '*.cfc' ); for ( var cfcOSPath in cfcs ) { - var cfcPath = replace( cfcOSPath, '\', '/', 'all' ); + var cfcPath = replace( cfcOSPath, chr(92), '/', 'all' ); // watch out for excluded paths: var excludePath = false; for ( var pattern in variables.config.exclude ) { @@ -329,6 +363,19 @@ component { } + private any function forceCache( any bean, string beanName) { + var info = variables.beanInfo[ beanName ]; + if ( info.isSingleton ) { + // cache on the qualified bean name: + var qualifiedName = beanName; + if ( structKeyExists( info, 'name' ) && structKeyExists( info, 'qualifier' ) ) { + qualifiedName = info.name & info.qualifier; + } + variables.beanCache[ qualifiedName ] = bean; + } + } + + private void function logMissingBean( string beanName, string resolvingBeanName = '' ) { var sys = createObject( 'java', 'java.lang.System' ); if ( len( resolvingBeanName ) ) { @@ -350,6 +397,24 @@ component { logMissingBean( beanName, resolvingBeanName ); } } + + + private void function onLoadEvent() { + var head = variables.listeners; + while ( isStruct( head ) ) { + if ( isCustomFunction( head.listener ) ) { + head.listener( this ); + } else if ( isObject( head.listener ) ) { + head.listener.onLoad( this ); + } else if ( isSimpleValue( head.listener ) && + containsBean( head.listener ) ) { + getBean( head.listener ).onLoad( this ); + } else { + throw "invalid onLoad listener registered: #head.listener.toString()#"; + } + head = head.next; + } + } private any function resolveBean( string beanName ) { @@ -381,31 +446,39 @@ component { if ( structKeyExists( variables.beanInfo, beanName ) ) { var info = variables.beanInfo[ beanName ]; if ( structKeyExists( info, 'cfc' ) ) { - // use createObject so we have control over initialization: - bean = createObject( 'component', info.cfc ); - if ( structKeyExists( info.metadata, 'constructor' ) ) { - var args = { }; - for ( var arg in info.metadata.constructor ) { - var argBean = resolveBeanCreate( arg, accumulator ); - // this throws a non-intuitive exception unless we step in... - if ( !structKeyExists( argBean, 'bean' ) ) { - throw 'bean not found: #arg#; while resolving constructor arguments for #beanName#'; + var metaBean = cachable( beanName ); + bean = metaBean.bean; + if ( metaBean.newObject ) { + if ( structKeyExists( info.metadata, 'constructor' ) ) { + var args = { }; + for ( var arg in info.metadata.constructor ) { + var argBean = resolveBeanCreate( arg, accumulator ); + // this throws a non-intuitive exception unless we step in... + if ( structKeyExists( argBean, 'bean' ) ) { + args[ arg ] = argBean.bean; + } else if ( info.metadata.constructor[ arg ] ) { + throw 'bean not found: #arg#; while resolving constructor arguments for #beanName#'; + } + } + var __ioc_newBean = evaluate( 'bean.init( argumentCollection = args )' ); + // if the constructor returns anything, it becomes the bean + // this allows for smart constructors that return things other + // than the CFC being created, such as implicit factory beans + // and automatic singletons etc (rare practices in CFML but...) + if ( isDefined( '__ioc_newBean' ) ) { + bean = __ioc_newBean; + forceCache( bean, beanName ); } - args[ arg ] = argBean.bean; } - var __ioc_newBean = evaluate( 'bean.init( argumentCollection = args )' ); - // if the constructor returns anything, it becomes the bean - // this allows for smart constructors that return things other - // than the CFC being created, such as implicit factory beans - // and automatic singletons etc (rare practices in CFML but...) - if ( isDefined( '__ioc_newBean' ) ) bean = __ioc_newBean; - } - var setterMeta = findSetters( bean, info.metadata ); - setterMeta.bean = bean; - accumulator.injection[ beanName ] = setterMeta; - for ( var property in setterMeta.setters ) { - resolveBeanCreate( property, accumulator ); } + if ( !structKeyExists( accumulator.injection, beanName ) ) { + var setterMeta = findSetters( bean, info.metadata ); + setterMeta.bean = bean; + accumulator.injection[ beanName ] = setterMeta; + for ( var property in setterMeta.setters ) { + resolveBeanCreate( property, accumulator ); + } + } accumulator.bean = bean; } else if ( structKeyExists( info, 'value' ) ) { accumulator.bean = info.value; @@ -432,7 +505,7 @@ component { variables.config.exclude = [ ]; } for ( var elem in variables.autoExclude ) { - arrayAppend( variables.config.exclude, replace( elem, '\', '/', 'all' ) ); + arrayAppend( variables.config.exclude, replace( elem, chr(92), '/', 'all' ) ); } // install bean factory constant: @@ -449,8 +522,8 @@ component { variables.transients[ transientFolder ] = true; } } - - variables.config.version = '0.1.6'; + + variables.config.version = '0.4.0'; } @@ -468,4 +541,4 @@ component { return single; } -} \ No newline at end of file +} diff --git a/examples/userManager/Application.cfc b/examples/userManager/Application.cfc index 2327854f..703e3fd4 100644 --- a/examples/userManager/Application.cfc +++ b/examples/userManager/Application.cfc @@ -1,19 +1,18 @@ - +component extends="org.corfield.framework" { - this.mappings["/userManager"] = getDirectoryFromPath(getCurrentTemplatePath()); this.name = 'fw1-userManager'; // FW/1 - configuration: variables.framework = { home = "user.default", - suppressImplicitService = false + suppressImplicitService = false, + trace = true }; function setupApplication() { setBeanFactory(createObject("component", "model.ObjectFactory").init(expandPath("./assets/config/beans.xml.cfm"))); } - - \ No newline at end of file +} diff --git a/org/corfield/framework.cfc b/org/corfield/framework.cfc index bbf0ad97..28ecb0f5 100644 --- a/org/corfield/framework.cfc +++ b/org/corfield/framework.cfc @@ -1,6 +1,6 @@ component { /* - Copyright (c) 2009-2011, Sean Corfield, Ryan Cogswell + Copyright (c) 2009-2012, Sean Corfield, Ryan Cogswell Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -23,15 +23,24 @@ component { variables.cgiScriptName = CGI.SCRIPT_NAME; variables.cgiPathInfo = CGI.PATH_INFO; } - request._fw1 = { }; + request._fw1 = { + cgiScriptName = CGI.SCRIPT_NAME, + cgiRequestMethod = CGI.REQUEST_METHOD, + controllers = [ ], + requestDefaultsInitialized = false, + services = [ ], + trace = [ ] + }; // do not rely on these, they are meant to be true magic... + variables.magicApplicationSubsystem = ']['; variables.magicApplicationController = '[]'; variables.magicApplicationAction = '__'; variables.magicBaseURL = '-[]-'; public void function abortController() { request._fw1.abortController = true; - throw( type="FW1.AbortControllerException", message="abortController() called" ); + frameworkTrace( 'abortController() called' ); + throw( type='FW1.AbortControllerException', message='abortController() called' ); } public boolean function actionSpecifiesSubsystem( string action ) { @@ -76,7 +85,7 @@ component { } } if ( path == 'useCgiScriptName' ) { - path = CGI.SCRIPT_NAME; + path = request._fw1.cgiScriptName; if ( variables.framework.SESOmitIndex ) { path = getDirectoryFromPath( path ); omitIndex = true; @@ -137,7 +146,7 @@ component { } else { initialDelim = '&'; } - } else if ( structKeyExists( request, 'generateSES' ) && request.generateSES ) { + } else if ( structKeyExists( request._fw1, 'generateSES' ) && request._fw1.generateSES ) { if ( omitIndex ) { initialDelim = ''; } else { @@ -233,20 +242,20 @@ component { var item = getItem( action ); var tuple = { }; - if ( structKeyExists( request, 'controllerExecutionStarted' ) ) { - raiseException( type="FW1.controllerExecutionStarted", message="Controller '#action#' may not be added at this point.", - detail="The controller execution phase has already started. Controllers may not be added by other controller methods." ); + if ( structKeyExists( request._fw1, 'controllerExecutionStarted' ) ) { + raiseException( type='FW1.controllerExecutionStarted', message="Controller '#action#' may not be added at this point.", + detail='The controller execution phase has already started. Controllers may not be added by other controller methods.' ); } tuple.controller = getController( section = section, subsystem = subsystem ); tuple.key = subsystem & variables.framework.subsystemDelimiter & section; + tuple.subsystem = subsystem; + tuple.section = section; tuple.item = item; if ( structKeyExists( tuple, 'controller' ) && isObject( tuple.controller ) ) { - if ( !structKeyExists( request, 'controllers' ) ) { - request.controllers = [ ]; - } - arrayAppend( request.controllers, tuple ); + frameworkTrace( 'queuing controller', subsystem, section, item ); + arrayAppend( request._fw1.controllers, tuple ); } } @@ -329,14 +338,20 @@ component { } if ( variables.framework.defaultSubsystem == '' ) { - raiseException( type="FW1.subsystemNotSpecified", message="No subsystem specified and no default configured.", - detail="When using subsystems, every request should specify a subsystem or variables.framework.defaultSubsystem should be configured." ); + raiseException( type='FW1.subsystemNotSpecified', message='No subsystem specified and no default configured.', + detail='When using subsystems, every request should specify a subsystem or variables.framework.defaultSubsystem should be configured.' ); } return variables.framework.defaultSubsystem; } + /* + * override this to provide your environment selector + */ + public string function getEnvironment() { + return ''; + } /* * return an action with all applicable parts (subsystem, section, and item) specified @@ -349,7 +364,13 @@ component { return getSectionAndItem( action ); } - + + /* + * return the local hostname of the server + */ + public string function getHostname() { + return createObject( 'java', 'java.net.InetAddress' ).getLocalHost().getHostName(); + } /* * return the item part of the action @@ -436,6 +457,13 @@ component { } return getDefaultSubsystem(); } + + /* + * return the base directory for the current request's subsystem + */ + public string function getSubsystemBase() { + return request.subsystemBase; + } /* * return the (optional) configuration for a subsystem @@ -510,6 +538,7 @@ component { */ public string function layout( string path, string body ) { var layoutPath = parseViewOrLayoutPath( path, 'layout' ); + frameworkTrace( 'layout( #path# ) called - rendering #viewPath#' ); return internalLayout( layoutPath, body ); } @@ -534,8 +563,14 @@ component { * in the code... */ public void function onError( any exception, string event ) { - try { + if ( !structKeyExists( variables, 'framework' ) || + !structKeyExists( variables.framework, 'version' ) ) { + // error occurred before framework was initialized + failure( exception, event, false, true ); + return; + } + // record details of the exception: if ( structKeyExists( request, 'action' ) ) { request.failedAction = request.action; @@ -543,11 +578,14 @@ component { request.exception = exception; request.event = event; // reset lifecycle flags: - structDelete( request, 'controllerExecutionComplete' ); - structDelete( request, 'controllerExecutionStarted' ); - structDelete( request, 'serviceExecutionComplete' ); + structDelete( request._fw1, 'controllerExecutionComplete' ); + structDelete( request._fw1, 'controllerExecutionStarted' ); + structDelete( request._fw1, 'serviceExecutionComplete' ); + structDelete( request._fw1, 'overrideViewAction' ); // setup the new controller action, based on the error action: - structDelete( request, 'controllers' ); + request._fw1.controllers = [ ]; + // reset services for this new action: + request._fw1.services = [ ]; if ( structKeyExists( variables, 'framework' ) && structKeyExists( variables.framework, 'error' ) ) { request.action = variables.framework.error; @@ -561,12 +599,28 @@ component { if ( !structKeyExists( request, 'context' ) ) { request.context = { }; } - + if ( !structKeyExists( request, 'base' ) ) { + if ( structKeyExists( variables, 'framework' ) && structKeyExists( variables.framework, 'base' ) ) { + request.base = variables.framework.base; + } else { + request.base = ''; + } + } + if ( !structKeyExists( request, 'cfcbase' ) ) { + if ( structKeyExists( variables, 'framework' ) && structKeyExists( variables.framework, 'cfcbase' ) ) { + request.cfcbase = variables.framework.cfcbase; + } else { + request.cfcbase = ''; + } + } + frameworkTrace( 'onError( #exception.message#, #event# ) called' ); setupRequestWrapper( false ); onRequest( '' ); + frameworkTraceRender(); } catch ( any e ) { failure( e, 'onError' ); failure( exception, event, true ); + frameworkTraceRender(); } } @@ -575,11 +629,12 @@ component { * this can be overridden if you want to change the behavior when * FW/1 cannot find a matching view */ - public void function onMissingView( struct rc ) { + public string function onMissingView( struct rc ) { // unable to find a matching view - fail with a nice exception viewNotFound(); // if we got here, we would return the string to be rendered // but viewNotFound() throws an exception... + // for example, return view( 'main.missing' ); } /* @@ -606,76 +661,88 @@ component { var once = { }; var n = 0; - request.controllerExecutionStarted = true; + request._fw1.controllerExecutionStarted = true; try { - if ( structKeyExists( request, 'controllers' ) ) { - n = arrayLen( request.controllers ); - for ( i = 1; i <= n; i = i + 1 ) { - tuple = request.controllers[ i ]; - // run before once per controller: - if ( !structKeyExists( once, tuple.key ) ) { - once[ tuple.key ] = i; - doController( tuple.controller, 'before' ); - if ( structKeyExists( request._fw1, "abortController" ) ) abortController(); - } - doController( tuple.controller, 'start' & tuple.item ); - if ( structKeyExists( request._fw1, "abortController" ) ) abortController(); - doController( tuple.controller, tuple.item ); - if ( structKeyExists( request._fw1, "abortController" ) ) abortController(); + n = arrayLen( request._fw1.controllers ); + for ( i = 1; i <= n; i = i + 1 ) { + tuple = request._fw1.controllers[ i ]; + // run before once per controller: + if ( !structKeyExists( once, tuple.key ) ) { + once[ tuple.key ] = i; + doController( tuple, 'before', 'before' ); + if ( structKeyExists( request._fw1, 'abortController' ) ) abortController(); } + doController( tuple, 'start' & tuple.item, 'start' ); + if ( structKeyExists( request._fw1, 'abortController' ) ) abortController(); + doController( tuple, tuple.item, 'item' ); + if ( structKeyExists( request._fw1, 'abortController' ) ) abortController(); } - n = arrayLen( request.services ); + n = arrayLen( request._fw1.services ); for ( i = 1; i <= n; i = i + 1 ) { - tuple = request.services[i]; + tuple = request._fw1.services[ i ]; if ( tuple.key == '' ) { // throw the result away: - doService( tuple.service, tuple.item, tuple.args, tuple.enforceExistence ); - if ( structKeyExists( request._fw1, "abortController" ) ) abortController(); + doService( tuple, tuple.item, tuple.args, tuple.enforceExistence ); + if ( structKeyExists( request._fw1, 'abortController' ) ) abortController(); } else { - _data_fw1 = doService( tuple.service, tuple.item, tuple.args, tuple.enforceExistence ); - if ( structKeyExists( request._fw1, "abortController" ) ) abortController(); + _data_fw1 = doService( tuple, tuple.item, tuple.args, tuple.enforceExistence ); + if ( structKeyExists( request._fw1, 'abortController' ) ) abortController(); if ( isDefined('_data_fw1') ) { + frameworkTrace( 'store service result in rc.#tuple.key#', tuple.subsystem, tuple.section, tuple.item ); request.context[ tuple.key ] = _data_fw1; - } + } else { + frameworkTrace( 'service returned no result for rc.#tuple.key#', tuple.subsystem, tuple.section, tuple.item ); + } } } - request.serviceExecutionComplete = true; - if ( structKeyExists( request, 'controllers' ) ) { - n = arrayLen( request.controllers ); - for ( i = n; i >= 1; i = i - 1 ) { - tuple = request.controllers[ i ]; - doController( tuple.controller, 'end' & tuple.item ); - if ( structKeyExists( request._fw1, "abortController" ) ) abortController(); - if ( once[ tuple.key ] eq i ) { - doController( tuple.controller, 'after' ); - if ( structKeyExists( request._fw1, "abortController" ) ) abortController(); - } + request._fw1.serviceExecutionComplete = true; + n = arrayLen( request._fw1.controllers ); + for ( i = n; i >= 1; i = i - 1 ) { + tuple = request._fw1.controllers[ i ]; + doController( tuple, 'end' & tuple.item, 'end' ); + if ( structKeyExists( request._fw1, 'abortController' ) ) abortController(); + if ( once[ tuple.key ] eq i ) { + doController( tuple, 'after', 'after' ); + if ( structKeyExists( request._fw1, 'abortController' ) ) abortController(); } } } catch ( FW1.AbortControllerException e ) { - request.serviceExecutionComplete = true; + request._fw1.serviceExecutionComplete = true; } - request.controllerExecutionComplete = true; - - buildViewAndLayoutQueue(); + request._fw1.controllerExecutionComplete = true; + buildViewQueue(); + frameworkTrace( 'setupView() called' ); setupView(); - - if ( structKeyExists(request, 'view') ) { - out = internalView( request.view ); + if ( structKeyExists(request._fw1, 'view') ) { + frameworkTrace( 'rendering #request._fw1.view#' ); + out = internalView( request._fw1.view ); } else { + frameworkTrace( 'onMissingView() called' ); out = onMissingView( request.context ); } - for ( i = 1; i <= arrayLen(request.layouts); i = i + 1 ) { + + buildLayoutQueue(); + for ( i = 1; i <= arrayLen(request._fw1.layouts); i = i + 1 ) { if ( structKeyExists(request, 'layout') && !request.layout ) { + frameworkTrace( 'aborting layout rendering' ); break; } - out = internalLayout( request.layouts[i], out ); + frameworkTrace( 'rendering #request._fw1.layouts[i]#' ); + out = internalLayout( request._fw1.layouts[i], out ); } writeOutput( out ); setupResponseWrapper(); } + /* + * if you override onRequestEnd(), call super.onRequestEnd() if you + * want tracing functionality to continue working + */ + public any function onRequestEnd() { + frameworkTraceRender(); + } + /* * it is better to set up your request configuration in * your setupRequest() method @@ -683,9 +750,6 @@ component { * super.onRequestStart() first */ public any function onRequestStart( string targetPath ) { - - var pathInfo = variables.cgiPathInfo; - setupFrameworkDefaults(); setupRequestDefaults(); @@ -693,59 +757,6 @@ component { setupApplicationWrapper(); } - if ( !structKeyExists(request, 'context') ) { - request.context = { }; - } - // SES URLs by popular request :) - if ( len( pathInfo ) > len( variables.cgiScriptName ) && left( pathInfo, len( variables.cgiScriptName ) ) == variables.cgiScriptName ) { - // canonicalize for IIS: - pathInfo = right( pathInfo, len( pathInfo ) - len( variables.cgiScriptName ) ); - } else if ( len( pathInfo ) > 0 && pathInfo == left( variables.cgiScriptName, len( pathInfo ) ) ) { - // pathInfo is bogus so ignore it: - pathInfo = ''; - } - pathInfo = processRoutes( pathInfo ); - try { - // we use .split() to handle empty items in pathInfo - we fallback to listToArray() on - // any system that doesn't support .split() just in case (empty items won't work there!) - if ( len( pathInfo ) > 1 ) { - pathInfo = right( pathInfo, len( pathInfo ) - 1 ).split( '/' ); - } else { - pathInfo = arrayNew( 1 ); - } - } catch ( any exception ) { - pathInfo = listToArray( pathInfo, '/' ); - } - var sesN = arrayLen( pathInfo ); - if ( ( sesN > 0 || variables.framework.generateSES ) && getBaseURL() != 'useRequestURI' ) { - request.generateSES = true; - } - for ( var sesIx = 1; sesIx <= sesN; sesIx = sesIx + 1 ) { - if ( sesIx == 1 ) { - request.context[variables.framework.action] = pathInfo[sesIx]; - } else if ( sesIx == 2 ) { - request.context[variables.framework.action] = pathInfo[sesIx-1] & '.' & pathInfo[sesIx]; - } else if ( sesIx mod 2 == 1 ) { - request.context[ pathInfo[sesIx] ] = ''; - } else { - request.context[ pathInfo[sesIx-1] ] = pathInfo[sesIx]; - } - } - // certain remote calls do not have URL or form scope: - if ( isDefined('URL') ) structAppend(request.context,URL); - if ( isDefined('form') ) structAppend(request.context,form); - // figure out the request action before restoring flash context: - if ( !structKeyExists(request.context, variables.framework.action) ) { - request.context[variables.framework.action] = variables.framework.home; - } else { - request.context[variables.framework.action] = getFullyQualifiedAction( request.context[variables.framework.action] ); - } - if ( variables.framework.noLowerCase ) { - request.action = validateAction( request.context[variables.framework.action] ); - } else { - request.action = validateAction( lCase(request.context[variables.framework.action]) ); - } - restoreFlashContext(); // ensure flash context cannot override request action: request.context[variables.framework.action] = request.action; @@ -753,12 +764,16 @@ component { // allow configured extensions and paths to pass through to the requested template. // NOTE: for unhandledPaths, we make the list into an escaped regular expression so we match on subdirectories. // Meaning /myexcludepath will match '/myexcludepath' and all subdirectories - if ( listFindNoCase( framework.unhandledExtensions, listLast( targetPath, '.' ) ) || - REFindNoCase( '^(' & framework.unhandledPathRegex & ')', targetPath ) ) { + if ( listFindNoCase( variables.framework.unhandledExtensions, listLast( targetPath, '.' ) ) || + REFindNoCase( '^(' & variables.framework.unhandledPathRegex & ')', targetPath ) ) { structDelete(this, 'onRequest'); structDelete(variables, 'onRequest'); - structDelete(this, 'onError'); - structDelete(variables, 'onError'); + structDelete(this, 'onRequestEnd'); + structDelete(variables, 'onRequestEnd'); + if ( !variables.framework.unhandledErrorCaught ) { + structDelete(this, 'onError'); + structDelete(variables, 'onError'); + } } else { setupRequestWrapper( true ); } @@ -773,11 +788,11 @@ component { public any function onSessionStart() { setupFrameworkDefaults(); setupRequestDefaults(); - setupSession(); + setupSessionWrapper(); } // populate() may be invoked inside controllers - public any function populate( any cfc, string keys = '', boolean trustKeys = false, boolean trim = false ) { + public any function populate( any cfc, string keys = '', boolean trustKeys = false, boolean trim = false, deep = false ) { if ( keys == '' ) { if ( trustKeys ) { // assume everything in the request context can be set into the CFC @@ -786,8 +801,8 @@ component { var args = { }; args[ property ] = request.context[ property ]; if ( trim && isSimpleValue( args[ property ] ) ) args[ property ] = trim( args[ property ] ); - // cfc[ 'set'&property ]( argumentCollection = args ); // ugh! no portable script version of this?!?! - evaluate( 'cfc.set#property#( argumentCollection = args )' ); + // cfc[ 'set'&property ]( argumentCollection = args ); // ugh! no portable script version of this?!?! + setProperty( cfc, property, args ); } catch ( any e ) { onPopulateError( cfc, property, request.context ); } @@ -800,7 +815,18 @@ component { args[ property ] = request.context[ property ]; if ( trim && isSimpleValue( args[ property ] ) ) args[ property ] = trim( args[ property ] ); // cfc[ 'set'&property ]( argumentCollection = args ); // ugh! no portable script version of this?!?! - evaluate( 'cfc.set#property#( argumentCollection = args )' ); + setProperty( cfc, property, args ); + } else if ( deep && structKeyExists( cfc, 'get' & property ) ) { + //look for a context property that starts with the property + for ( var key in request.context ) { + if ( listFindNoCase( key, property, '.') ) { + try { + setProperty( cfc, key, { '#key#' = request.context[ key ] } ); + } catch ( any e ) { + onPopulateError( cfc, key, request.context); + } + } + } } } } @@ -815,16 +841,44 @@ component { args[ trimProperty ] = request.context[ trimProperty ]; if ( trim && isSimpleValue( args[ trimProperty ] ) ) args[ trimProperty ] = trim( args[ trimProperty ] ); // cfc[ 'set'&trimproperty ]( argumentCollection = args ); // ugh! no portable script version of this?!?! - evaluate( 'cfc.set#trimProperty#( argumentCollection = args )' ); + setProperty( cfc, trimProperty, args ); + } + } else if ( deep ) { + if ( listLen( trimProperty, '.' ) > 1 ) { + var prop = listFirst( trimProperty, '.' ); + if ( structKeyExists( cfc, 'get' & prop ) ) { + setProperty( cfc, trimProperty, { '#trimProperty#' = request.context[ trimProperty ] } ); + } } } } } return cfc; } + + private void function setProperty( struct cfc, string property, struct args ) { + if ( listLen( property, '.' ) > 1 ) { + var firstObjName = listFirst( property, '.' ); + var newProperty = listRest( property, '.' ); + + args[ newProperty ] = args[ property ]; + structDelete( args, property ); + + if ( structKeyExists( cfc , 'get' & firstObjName ) ) { + var obj = getProperty( cfc, firstObjName ); + if ( !isNull( obj ) ) setProperty( obj, newProperty, args ); + } + } else { + evaluate( 'cfc.set#property#( argumentCollection = args )' ); + } + } + private any function getProperty( struct cfc, string property ) { + if ( structKeyExists( cfc, 'get#property#' ) ) return evaluate( 'cfc.get#property#()' ); + } + // call from your controller to redirect to a clean URL based on an action, pushing data to flash scope if necessary: - public void function redirect( string action, string preserve = 'none', string append = 'none', string path = variables.magicBaseURL, string queryString = '' ) { + public void function redirect( string action, string preserve = 'none', string append = 'none', string path = variables.magicBaseURL, string queryString = '', string statusCode = '302' ) { if ( path == variables.magicBaseURL ) path = getBaseURL(); var preserveKey = ''; if ( preserve != 'none' ) { @@ -875,7 +929,11 @@ component { } } setupResponseWrapper(); - location( targetURL, false ); + if ( variables.framework.trace ) { + frameworkTrace( 'redirecting to #targetURL# (#statusCode#)' ); + session._fw1_trace = request._fw1.trace; + } + location( targetURL, false, statusCode ); } // call this from your controller to queue up additional services @@ -885,21 +943,24 @@ component { var item = getItem( action ); var tuple = { }; - if ( structKeyExists( request, "serviceExecutionComplete" ) ) { - raiseException( type="FW1.serviceExecutionComplete", message="Service '#action#' may not be added at this point.", - detail="The service execution phase is complete. Services may not be added by end*() or after() controller methods." ); + if ( structKeyExists( request._fw1, 'serviceExecutionComplete' ) ) { + raiseException( type='FW1.serviceExecutionComplete', message="Service '#action#' may not be added at this point.", + detail='The service execution phase is complete. Services may not be added by end*() or after() controller methods.' ); } tuple.service = getService(section=section, subsystem=subsystem); + tuple.subsystem = subsystem; + tuple.section = section; tuple.item = item; tuple.key = key; tuple.args = args; tuple.enforceExistence = enforceExistence; - if ( structKeyExists( tuple, "service" ) && isObject( tuple.service ) ) { - arrayAppend( request.services, tuple ); + if ( structKeyExists( tuple, 'service' ) && isObject( tuple.service ) ) { + frameworkTrace( 'queuing service', subsystem, section, item ); + arrayAppend( request._fw1.services, tuple ); } else if ( enforceExistence ) { - raiseException( type="FW1.serviceCfcNotFound", message="Service '#action#' does not exist.", + raiseException( type='FW1.serviceCfcNotFound', message="Service '#action#' does not exist.", detail="To have the execution of this service be conditional based upon its existence, pass in a third parameter of 'false'." ); } } @@ -919,7 +980,7 @@ component { * use this to override the default layout */ public void function setLayout( string action ) { - request.overrideLayoutAction = validateAction( action ); + request._fw1.overrideLayoutAction = validateAction( action ); } /* @@ -944,6 +1005,12 @@ component { */ public void function setupApplication() { } + /* + * override this to provide environment-specific initialization + * you do not need to call super.setupEnvironment() + */ + public void function setupEnvironment( string env ) { } + /* * override this to provide request-specific initialization * you do not need to call super.setupRequest() @@ -983,7 +1050,7 @@ component { * use this to override the default view */ public void function setView( string action ) { - request.overrideViewAction = validateAction( action ); + request._fw1.overrideViewAction = validateAction( action ); } /* @@ -998,7 +1065,8 @@ component { * returns the UI generated by the named view */ public string function view( string path, struct args = { } ) { - var viewPath = parseViewOrLayoutPath( path, "view" ); + var viewPath = parseViewOrLayoutPath( path, 'view' ); + frameworkTrace( 'view( #path# ) called - rendering #viewPath#' ); return internalView( viewPath, args ); } @@ -1016,7 +1084,7 @@ component { } } - private void function buildViewAndLayoutQueue() { + private void function buildLayoutQueue() { var siteWideLayoutBase = request.base & getSubsystemDirPrefix( variables.framework.siteWideLayoutSubsystem ); var testLayout = 0; // default behavior: @@ -1025,77 +1093,95 @@ component { var item = request.item; var subsystembase = ''; - // has view been overridden? - if ( structKeyExists( request, 'overrideViewAction' ) ) { - subsystem = getSubsystem( request.overrideViewAction ); - section = getSection( request.overrideViewAction ); - item = getItem( request.overrideViewAction ); - structDelete( request, 'overrideViewAction' ); - } - subsystembase = request.base & getSubsystemDirPrefix( subsystem ); - - // view and layout setup - used to be in setupRequestWrapper(): - request.view = parseViewOrLayoutPath( subsystem & variables.framework.subsystemDelimiter & - section & '/' & item, 'view' ); - if ( !cachedFileExists( expandPath( request.view ) ) ) { - request.missingView = request.view; - // ensures original view not re-invoked for onError() case: - structDelete( request, 'view' ); - } - - request.layouts = [ ]; + request._fw1.layouts = [ ]; // has layout been overridden? - if ( structKeyExists( request, 'overrideLayoutAction' ) ) { - subsystem = getSubsystem( request.overrideLayoutAction ); - section = getSection( request.overrideLayoutAction ); - item = getItem( request.overrideLayoutAction ); - structDelete( request, 'overrideLayoutAction' ); + if ( structKeyExists( request._fw1, 'overrideLayoutAction' ) ) { + subsystem = getSubsystem( request._fw1.overrideLayoutAction ); + section = getSection( request._fw1.overrideLayoutAction ); + item = getItem( request._fw1.overrideLayoutAction ); + structDelete( request._fw1, 'overrideLayoutAction' ); } subsystembase = request.base & getSubsystemDirPrefix( subsystem ); - + frameworkTrace( 'building layout queue', subsystem, section, item ); // look for item-specific layout: testLayout = parseViewOrLayoutPath( subsystem & variables.framework.subsystemDelimiter & section & '/' & item, 'layout' ); - if ( cachedFileExists( expandPath( testLayout ) ) ) { - arrayAppend( request.layouts, testLayout ); - } + if ( cachedFileExists( testLayout ) ) { + frameworkTrace( 'found item-specific layout #testLayout#', subsystem, section, item ); + arrayAppend( request._fw1.layouts, testLayout ); + } // look for section-specific layout: testLayout = parseViewOrLayoutPath( subsystem & variables.framework.subsystemDelimiter & section, 'layout' ); - if ( cachedFileExists( expandPath( testLayout ) ) ) { - arrayAppend( request.layouts, testLayout ); + if ( cachedFileExists( testLayout ) ) { + frameworkTrace( 'found section-specific layout #testLayout#', subsystem, section, item ); + arrayAppend( request._fw1.layouts, testLayout ); } // look for subsystem-specific layout (site-wide layout if not using subsystems): if ( request.section != 'default' ) { testLayout = parseViewOrLayoutPath( subsystem & variables.framework.subsystemDelimiter & 'default', 'layout' ); - if ( cachedFileExists( expandPath( testLayout ) ) ) { - arrayAppend( request.layouts, testLayout ); + if ( cachedFileExists( testLayout ) ) { + frameworkTrace( 'found default layout #testLayout#', subsystem, section, item ); + arrayAppend( request._fw1.layouts, testLayout ); } } // look for site-wide layout (only applicable if using subsystems) if ( usingSubsystems() && siteWideLayoutBase != subsystembase ) { testLayout = parseViewOrLayoutPath( variables.framework.siteWideLayoutSubsystem & variables.framework.subsystemDelimiter & 'default', 'layout' ); - if ( cachedFileExists( expandPath( testLayout ) ) ) { - arrayAppend( request.layouts, testLayout ); + if ( cachedFileExists( testLayout ) ) { + frameworkTrace( 'found #variables.framework.siteWideLayoutSubsystem# layout #testLayout#', subsystem, section, item ); + arrayAppend( request._fw1.layouts, testLayout ); } } } + + private void function buildViewQueue() { + // default behavior: + var subsystem = request.subsystem; + var section = request.section; + var item = request.item; + var subsystembase = ''; + + // has view been overridden? + if ( structKeyExists( request._fw1, 'overrideViewAction' ) ) { + subsystem = getSubsystem( request._fw1.overrideViewAction ); + section = getSection( request._fw1.overrideViewAction ); + item = getItem( request._fw1.overrideViewAction ); + structDelete( request._fw1, 'overrideViewAction' ); + } + subsystembase = request.base & getSubsystemDirPrefix( subsystem ); + frameworkTrace( 'building view queue', subsystem, section, item ); + // view and layout setup - used to be in setupRequestWrapper(): + request._fw1.view = parseViewOrLayoutPath( subsystem & variables.framework.subsystemDelimiter & + section & '/' & item, 'view' ); + if ( cachedFileExists( request._fw1.view ) ) { + frameworkTrace( 'found view #request._fw1.view#', subsystem, section, item ); + } else { + frameworkTrace( 'no such view #request._fw1.view#', subsystem, section, item ); + request.missingView = request._fw1.view; + // ensures original view not re-invoked for onError() case: + structDelete( request._fw1, 'view' ); + } + } + + private boolean function cachedFileExists( string filePath ) { var cache = application[ variables.framework.applicationKey ].cache; if ( !variables.framework.cacheFileExists ) { - return fileExists( filePath ); + return fileExists( expandPath( filePath) ); } param name="cache.fileExists" default="#{ }#"; if ( !structKeyExists( cache.fileExists, filePath ) ) { - cache.fileExists[ filePath ] = fileExists( filePath ); + cache.fileExists[ filePath ] = fileExists( expandPath( filePath ) ); } return cache.fileExists[ filePath ]; } + private string function cfcFilePath( string dottedPath ) { if ( dottedPath == '' ) { return '/'; @@ -1104,21 +1190,36 @@ component { } } - private void function doController( any cfc, string method ) { - if ( structKeyExists( cfc, method ) || structKeyExists( cfc, 'onMissingMethod' ) ) { + private void function doController( struct tuple, string method, string lifecycle ) { + var cfc = tuple.controller; + if ( structKeyExists( cfc, method ) ) { try { + frameworkTrace( 'calling #lifecycle# controller', tuple.subsystem, tuple.section, method ); evaluate( 'cfc.#method#( rc = request.context )' ); } catch ( any e ) { setCfcMethodFailureInfo( cfc, method ); rethrow; } } + else if ( structKeyExists( cfc, 'onMissingMethod' ) ) { + try { + frameworkTrace( 'calling #lifecycle# controller (via onMissingMethod)', tuple.subsystem, tuple.section, method ); + evaluate( 'cfc.#method#( rc = request.context, method = lifecycle )' ); + } catch ( any e ) { + setCfcMethodFailureInfo( cfc, method ); + rethrow; + } + } else { + frameworkTrace( 'no #lifecycle# controller to call', tuple.subsystem, tuple.section, method ); + } } - private any function doService( any cfc, string method, struct args, boolean enforceExistence ) { + private any function doService( struct tuple, string method, struct args, boolean enforceExistence ) { + var cfc = tuple.service; if ( structKeyExists( cfc, method ) || structKeyExists( cfc, 'onMissingMethod' ) ) { try { structAppend( args, request.context, false ); + frameworkTrace( 'calling service', tuple.subsystem, tuple.section, method ); var _result_fw1 = evaluate( 'cfc.#method#( argumentCollection = args )' ); if ( !isNull( _result_fw1 ) ) { return _result_fw1; @@ -1128,7 +1229,7 @@ component { rethrow; } } else if ( enforceExistence ) { - raiseException( type="FW1.serviceMethodNotFound", message="Service method '#method#' does not exist in service '#getMetadata( cfc ).fullname#'.", + raiseException( type='FW1.serviceMethodNotFound', message="Service method '#method#' does not exist in service '#getMetadata( cfc ).fullname#'.", detail="To have the execution of this service method be conditional based upon its existence, pass in a third parameter of 'false'." ); } } @@ -1151,17 +1252,23 @@ component { } - private void function failure( any exception, string event, boolean indirect = false ) { + private void function failure( any exception, string event, boolean indirect = false, boolean early = false ) { var h = indirect ? 3 : 1; if ( structKeyExists(exception, 'rootCause') ) { exception = exception.rootCause; } - writeOutput( "" & ( indirect ? "Original exception " : "Exception" ) & " in #event#" ); - if ( structKeyExists( request, 'failedAction' ) ) { - writeOutput( "

The action #request.failedAction# failed.

" ); + getPageContext().getResponse().setStatus( 500 ); + if ( arguments.early ) { + writeOutput( '

Exception occured before FW/1 was initialized

'); + } else { + writeOutput( '' & ( indirect ? 'Original exception ' : 'Exception' ) & ' in #event#' ); + if ( structKeyExists( request, 'failedAction' ) ) { + writeOutput( '

The action #request.failedAction# failed.

' ); + } + writeOutput( '#exception.message#' ); } - writeOutput( "#exception.message#" ); - writeOutput( "

#exception.detail# (#exception.type#)

" ); + + writeOutput( '

#exception.detail# (#exception.type#)

' ); dumpException(exception); } @@ -1214,6 +1321,64 @@ component { return setters; } + private void function frameworkTrace( string message, string subsystem = '', string section = '', string item = '' ) { + if ( variables.framework.trace ) { + if ( isDefined( 'session._fw1_trace' ) && structKeyExists( session, '_fw1_trace' ) ) { + request._fw1.trace = session._fw1_trace; + structDelete( session, '_fw1_trace' ); + } + arrayAppend( request._fw1.trace, { tick = getTickCount(), msg = message, sub = subsystem, s = section, i = item } ); + } + } + + private void function frameworkTraceRender() { + if ( variables.framework.trace && arrayLen( request._fw1.trace ) ) { + var startTime = request._fw1.trace[1].tick; + var font = 'font-family: verdana, helvetica;'; + writeOutput( '
' ); + writeOutput( '
Framework Lifecycle Trace
' ); + var table = ''; + writeOutput( table ); + var colors = [ '##ccd4dd', '##ccddcc' ]; + var row = 0; + var n = arrayLen( request._fw1.trace ); + for ( var i = 1; i <= n; ++i ) { + var trace = request._fw1.trace[i]; + var action = ''; + if ( trace.s == variables.magicApplicationController || trace.sub == variables.magicApplicationSubsystem ) { + action = 'Application.cfc'; + if ( right( trace.i, len( variables.magicApplicationAction ) ) == variables.magicApplicationAction ) { + continue; + } + } else { + action = trace.sub; + if ( action != '' && trace.s != '' ) { + action &= variables.framework.subsystemDelimiter; + } + action &= trace.s; + if ( trace.s != '' ) { + action &= '.'; + } + action &= trace.i; + } + ++row; + writeOutput( '' ); + writeOutput( '' ); + writeOutput( '' ); + var color = + trace.msg.startsWith( 'no ' ) ? '##cc8888' : + trace.msg.startsWith( 'onError( ' ) ? '##cc0000' : '##0000'; + writeOutput( '' ); + writeOutput( '' ); + if ( trace.msg.startsWith( 'redirecting ' ) ) { + writeOutput( '
#trace.tick - startTime#ms#action##trace.msg#
#table#' ); + if ( i < n ) startTime = request._fw1.trace[i+1].tick; + } + } + writeOutput( '' ); + } + } + private any function getCachedComponent( string type, string subsystem, string section ) { setupSubsystemWrapper( subsystem ); @@ -1239,7 +1404,7 @@ component { if ( type == 'controller' && section == variables.magicApplicationController ) { // treat this (Application.cfc) as a controller: cfc = this; - } else if ( cachedFileExists( expandPath( cfcFilePath( request.cfcbase ) & subsystemDir & types & '/' & section & '.cfc' ) ) ) { + } else if ( cachedFileExists( cfcFilePath( request.cfcbase ) & subsystemDir & types & '/' & section & '.cfc' ) ) { // we call createObject() rather than new so we can control initialization: if ( request.cfcbase == '' ) { cfc = createObject( 'component', subsystemDot & types & '.' & section ); @@ -1281,20 +1446,24 @@ component { private string function getNextPreserveKeyAndPurgeOld() { var nextPreserveKey = ''; var oldKeyToPurge = ''; - if ( variables.framework.maxNumContextsPreserved > 1 ) { - lock scope="session" type="exclusive" timeout="30" { - param name="session.__fw1NextPreserveKey" default="1"; - nextPreserveKey = session.__fw1NextPreserveKey; - session.__fw1NextPreserveKey = session.__fw1NextPreserveKey + 1; - } - oldKeyToPurge = nextPreserveKey - variables.framework.maxNumContextsPreserved; - } else { - lock scope="session" type="exclusive" timeout="30" { - session.__fw1PreserveKey = ''; - nextPreserveKey = session.__fw1PreserveKey; - } - oldKeyToPurge = ''; - } + try { + if ( variables.framework.maxNumContextsPreserved > 1 ) { + lock scope="session" type="exclusive" timeout="30" { + param name="session.__fw1NextPreserveKey" default="1"; + nextPreserveKey = session.__fw1NextPreserveKey; + session.__fw1NextPreserveKey = session.__fw1NextPreserveKey + 1; + } + oldKeyToPurge = nextPreserveKey - variables.framework.maxNumContextsPreserved; + } else { + lock scope="session" type="exclusive" timeout="30" { + session.__fw1PreserveKey = ''; + nextPreserveKey = session.__fw1PreserveKey; + } + oldKeyToPurge = ''; + } + } catch ( any e ) { + // ignore - assume session scope is disabled + } var key = getPreserveKeySessionKey( oldKeyToPurge ); if ( structKeyExists( session, key ) ) { structDelete( session, key ); @@ -1340,9 +1509,9 @@ component { if ( structKeyExists( rc, '$' ) ) { $ = rc.$; } - if ( !structKeyExists( request, 'controllerExecutionComplete' ) ) { - raiseException( type="FW1.layoutExecutionFromController", message="Invalid to call the layout method at this point.", - detail="The layout method should not be called prior to the completion of the controller execution phase." ); + if ( !structKeyExists( request._fw1, 'controllerExecutionComplete' ) ) { + raiseException( type='FW1.layoutExecutionFromController', message='Invalid to call the layout method at this point.', + detail='The layout method should not be called prior to the completion of the controller execution phase.' ); } var response = ''; savecontent variable="response" { @@ -1359,9 +1528,10 @@ component { $ = rc.$; } structAppend( local, args ); - if ( !structKeyExists( request, 'serviceExecutionComplete') && arrayLen( request.services ) != 0 ) { - raiseException( type="FW1.viewExecutionFromController", message="Invalid to call the view method at this point.", - detail="The view method should not be called prior to the completion of the service execution phase." ); + if ( !structKeyExists( request._fw1, 'serviceExecutionComplete') && + structKeyExists( request._fw1, 'services' ) && arrayLen( request._fw1.services ) != 0 ) { + raiseException( type='FW1.viewExecutionFromController', message='Invalid to call the view method at this point.', + detail='The view method should not be called prior to the completion of the service execution phase.' ); } var response = ''; savecontent variable="response" { @@ -1431,27 +1601,31 @@ component { } if ( route == '*' ) { route = '/'; - } else if ( right( route, 1 ) != '/' ) { + } else if ( right( route, 1 ) != '/' && right( route, 1 ) != '$' ) { + // only add the closing backslash if last position is not already a "/" or a "$" to respect regex end of string route &= '/'; } } else { route = '/'; } if ( !len( target ) || right( target, 1) != '/' ) target &= '/'; - // walk for :var and replace with ([^/]*) in route and back reference in target: + // walk for self defined (regex) and :var - replace :var with ([^/]*) in route and back reference in target: var n = 1; - var placeholders = rematch( ':[^/]+', route ); + var placeholders = rematch( '(:[^/]+)|(\([^\)]+)', route ); for ( var placeholder in placeholders ) { - route = replace( route, placeholder, '([^/]*)' ); - target = replace( target, placeholder, chr(92) & n ); + if ( left( placeholder, 1 ) == ':') { + route = replace( route, placeholder, '([^/]*)' ); + target = replace( target, placeholder, chr(92) & n ); + } ++n; } - // add trailing match/back reference: - route &= '(.*)'; + // add trailing match/back reference: if last character is not "$" to respect regex end of string + if (right( route, 1 ) != '$') + route &= '(.*)'; target &= chr(92) & n; // end of preprocessing section if ( !len( path ) || right( path, 1) != '/' ) path &= '/'; - var matched = len( routeMatch.method ) ? ( '$' & CGI.REQUEST_METHOD == routeMatch.method ) : true; + var matched = len( routeMatch.method ) ? ( '$' & request._fw1.cgiRequestMethod == routeMatch.method ) : true; if ( matched && reFind( route, path ) ) { routeMatch.matched = true; routeMatch.pattern = route; @@ -1575,6 +1749,7 @@ component { } // this will recreate the main bean factory on a reload: + frameworkTrace( 'setupApplication() called' ); setupApplication(); if ( isReload ) { @@ -1680,6 +1855,9 @@ component { if ( !structKeyExists(variables.framework, 'unhandledPaths') ) { variables.framework.unhandledPaths = '/flex2gateway'; } + if ( !structKeyExists( variables.framework, 'unhandledErrorCaught' ) ) { + variables.framework.unhandledErrorCaught = false; + } // convert unhandledPaths to regex: variables.framework.unhandledPathRegex = replaceNoCase( REReplace( variables.framework.unhandledPaths, '(\+|\*|\?|\.|\[|\^|\$|\(|\)|\{|\||\\)', '\\\1', 'all' ), @@ -1702,12 +1880,88 @@ component { if ( !structKeyExists( variables.framework, 'subsystems' ) ) { variables.framework.subsystems = { }; } - variables.framework.version = '2.0.1'; + if ( !structKeyExists( variables.framework, 'trace' ) ) { + variables.framework.trace = false; + } + variables.framework.version = '2.1'; + setupFrameworkEnvironments(); } + private void function setupFrameworkEnvironments() { + var env = getEnvironment(); + if ( structKeyExists( variables.framework, 'environments' ) ) { + var envs = variables.framework.environments; + var tier = listFirst( env, '-' ); + if ( structKeyExists( envs, tier ) ) { + structAppend( variables.framework, envs[ tier ] ); + } + if ( structKeyExists( envs, env ) ) { + structAppend( variables.framework, envs[ env ] ); + } + } + setupEnvironment( env ); + } + private void function setupRequestDefaults() { - request.base = variables.framework.base; - request.cfcbase = variables.framework.cfcbase; + if ( !request._fw1.requestDefaultsInitialized ) { + var pathInfo = variables.cgiPathInfo; + request.base = variables.framework.base; + request.cfcbase = variables.framework.cfcbase; + + if ( !structKeyExists(request, 'context') ) { + request.context = { }; + } + // SES URLs by popular request :) + if ( len( pathInfo ) > len( variables.cgiScriptName ) && left( pathInfo, len( variables.cgiScriptName ) ) == variables.cgiScriptName ) { + // canonicalize for IIS: + pathInfo = right( pathInfo, len( pathInfo ) - len( variables.cgiScriptName ) ); + } else if ( len( pathInfo ) > 0 && pathInfo == left( variables.cgiScriptName, len( pathInfo ) ) ) { + // pathInfo is bogus so ignore it: + pathInfo = ''; + } + pathInfo = processRoutes( pathInfo ); + try { + // we use .split() to handle empty items in pathInfo - we fallback to listToArray() on + // any system that doesn't support .split() just in case (empty items won't work there!) + if ( len( pathInfo ) > 1 ) { + pathInfo = right( pathInfo, len( pathInfo ) - 1 ).split( '/' ); + } else { + pathInfo = arrayNew( 1 ); + } + } catch ( any exception ) { + pathInfo = listToArray( pathInfo, '/' ); + } + var sesN = arrayLen( pathInfo ); + if ( ( sesN > 0 || variables.framework.generateSES ) && getBaseURL() != 'useRequestURI' ) { + request._fw1.generateSES = true; + } + for ( var sesIx = 1; sesIx <= sesN; sesIx = sesIx + 1 ) { + if ( sesIx == 1 ) { + request.context[variables.framework.action] = pathInfo[sesIx]; + } else if ( sesIx == 2 ) { + request.context[variables.framework.action] = pathInfo[sesIx-1] & '.' & pathInfo[sesIx]; + } else if ( sesIx mod 2 == 1 ) { + request.context[ pathInfo[sesIx] ] = ''; + } else { + request.context[ pathInfo[sesIx-1] ] = pathInfo[sesIx]; + } + } + // certain remote calls do not have URL or form scope: + if ( isDefined('URL') ) structAppend(request.context,URL); + if ( isDefined('form') ) structAppend(request.context,form); + // figure out the request action before restoring flash context: + if ( !structKeyExists(request.context, variables.framework.action) ) { + request.context[variables.framework.action] = variables.framework.home; + } else { + request.context[variables.framework.action] = getFullyQualifiedAction( request.context[variables.framework.action] ); + } + if ( variables.framework.noLowerCase ) { + request.action = validateAction( request.context[variables.framework.action] ); + } else { + request.action = validateAction( lCase(request.context[variables.framework.action]) ); + } + request._fw1.requestDefaultsInitialized = true; + } } private void function setupRequestWrapper( boolean runSetup ) { @@ -1716,12 +1970,17 @@ component { request.subsystembase = request.base & getSubsystemDirPrefix( request.subsystem ); request.section = getSection( request.action ); request.item = getItem( request.action ); - request.services = [ ]; if ( runSetup ) { rc = request.context; - controller( variables.magicApplicationController & '.' & variables.magicApplicationAction ); + if ( usingSubsystems() ) { + controller( variables.magicApplicationSubsystem & variables.framework.subsystemDelimiter & + variables.magicApplicationController & '.' & variables.magicApplicationAction ); + } else { + controller( variables.magicApplicationController & '.' & variables.magicApplicationAction ); + } setupSubsystemWrapper( request.subsystem ); + frameworkTrace( 'setupRequest() called' ); setupRequest(); } @@ -1732,10 +1991,12 @@ component { } private void function setupResponseWrapper() { + frameworkTrace( 'setupResponse() called' ); setupResponse(); } private void function setupSessionWrapper() { + frameworkTrace( 'setupSession() called' ); setupSession(); } @@ -1744,6 +2005,7 @@ component { lock name="fw1_#application.applicationName#_#variables.framework.applicationKey#_subsysteminit_#subsystem#" type="exclusive" timeout="30" { if ( !isSubsystemInitialized( subsystem ) ) { application[ variables.framework.applicationKey ].subsystems[ subsystem ] = now(); + frameworkTrace( 'setupSubsystem() called', subsystem ); setupSubsystem( subsystem ); } } @@ -1752,14 +2014,14 @@ component { private string function validateAction( string action ) { // check for forward and backward slash in the action - using chr() to avoid confusing TextMate (Hi Nathan!) if ( findOneOf( chr(47) & chr(92), action ) > 0 ) { - raiseException( type="FW1.actionContainsSlash", message="Found a slash in the action: '#action#'.", - detail="Actions are not allowed to embed sub-directory paths."); + raiseException( type='FW1.actionContainsSlash', message="Found a slash in the action: '#action#'.", + detail='Actions are not allowed to embed sub-directory paths.'); } return action; } private void function viewNotFound() { - raiseException( type="FW1.viewNotFound", message="Unable to find a view for '#request.action#' action.", + raiseException( type='FW1.viewNotFound', message="Unable to find a view for '#request.action#' action.", detail="'#request.missingView#' does not exist." ); } diff --git a/tests/Application.cfc b/tests/Application.cfc new file mode 100644 index 00000000..54f5b8ff --- /dev/null +++ b/tests/Application.cfc @@ -0,0 +1,6 @@ +component{ + this.name = 'fw1 test'; + + this.mappings['/mxunit'] = getDirectoryFromPath(getCurrentTemplatePath()) & "../../mxunit"; + this.mappings['/org'] = getDirectoryFromPath(getCurrentTemplatePath()) & "../org"; +} \ No newline at end of file diff --git a/tests/InjectableTest.cfc b/tests/InjectableTest.cfc new file mode 100644 index 00000000..3bb42d40 --- /dev/null +++ b/tests/InjectableTest.cfc @@ -0,0 +1,14 @@ +component extends="mxunit.framework.TestCase" { + + private any function getVariablesScope( any cfc ) { + cfc.__$$fetchVariables = returnVariablesScope; + var vars = cfc.__$$fetchVariables(); + structDelete( cfc, "__$$fetchVariables" ); + return vars; + } + + private any function returnVariablesScope() { + return variables; + } + +} diff --git a/tests/frameworkEnvTest.cfc b/tests/frameworkEnvTest.cfc new file mode 100644 index 00000000..fc6418f0 --- /dev/null +++ b/tests/frameworkEnvTest.cfc @@ -0,0 +1,162 @@ +component extends="tests.InjectableTest" { + + public void function setUp() { + structClear( request ); + variables.fw = new org.corfield.framework(); + variables.fwvars = getVariablesScope( variables.fw ); + variables.fwvars.framework = { }; + variables.fwcfg = variables.fwvars.framework; + variables.fwcfg.environments = { + "dev" = { reloadApplicationOnEveryRequest = true }, + "dev-one" = { oneNewOption = 1 }, + "dev-two" = { reloadApplicationOnEveryRequest = false }, + "prod" = { useSSL = true } + }; + } + + public void function testGetEnvironmentIsCalled() { + variables.fw.getEnvironment = recordCalls; + variables.fwvars.getEnvironment = recordCalls; + variables.fw.__getEnvCalls = 0; + variables.fw.onRequestStart( "" ); + assertEquals( 1, variables.fw.__getEnvCalls ); + } + + private string function recordCalls() { + this.__getEnvCalls += 1; + return ""; + } + + public void function testSetupEnvironmentIsCalled() { + variables.fw.setupEnvironment = recordCallsWithArg; + variables.fwvars.setupEnvironment = recordCallsWithArg; + variables.fw.__setupEnvCalls = 0; + variables.fw.__setupEnvArgs = [ ]; + variables.fw.onRequestStart( "" ); + assertEquals( 1, variables.fw.__setupEnvCalls ); + assertEquals( "", variables.fw.__setupEnvArgs[ 1 ] ); + } + + public void function testSetupEnvironmentIsCalledWithEnv() { + variables.fw.getEnvironment = returnTierNoMatch; + variables.fwvars.getEnvironment = returnTierNoMatch; + variables.fw.setupEnvironment = recordCallsWithArg; + variables.fwvars.setupEnvironment = recordCallsWithArg; + variables.fw.__setupEnvCalls = 0; + variables.fw.__setupEnvArgs = [ ]; + variables.fw.onRequestStart( "" ); + assertEquals( 1, variables.fw.__setupEnvCalls ); + assertEquals( "I do not match any tier", variables.fw.__setupEnvArgs[ 1 ] ); + } + + private void function recordCallsWithArg( string env ) { + this.__setupEnvCalls += 1; + arrayAppend( this.__setupEnvArgs, env ); + } + + public void function testDefault() { + variables.fw.onRequestStart( "" ); + assertFalse( variables.fwcfg.reloadApplicationOnEveryRequest, "Reload should be default: false" ); + assertFalse( structKeyExists( variables.fwcfg, "oneNewOption" ), "OneNewOption should not have been added" ); + assertFalse( structKeyExists( variables.fwcfg, "useSSL" ), "UseSSL should not have been added" ); + } + + public void function testTierOnlyNoMatch() { + variables.fw.getEnvironment = returnTierNoMatch; + variables.fwvars.getEnvironment = returnTierNoMatch; + variables.fw.onRequestStart( "" ); + assertFalse( variables.fwcfg.reloadApplicationOnEveryRequest, "Reload should be default: false" ); + assertFalse( structKeyExists( variables.fwcfg, "oneNewOption" ), "OneNewOption should not have been added" ); + assertFalse( structKeyExists( variables.fwcfg, "useSSL" ), "UseSSL should not have been added" ); + } + + private string function returnTierNoMatch() { + return "I do not match any tier"; + } + + public void function testTierOnlyDev() { + variables.fw.getEnvironment = returnTierDev; + variables.fwvars.getEnvironment = returnTierDev; + variables.fw.onRequestStart( "" ); + assertTrue( variables.fwcfg.reloadApplicationOnEveryRequest, "Reload should be dev: true" ); + assertFalse( structKeyExists( variables.fwcfg, "oneNewOption" ), "OneNewOption should not have been added" ); + assertFalse( structKeyExists( variables.fwcfg, "useSSL" ), "UseSSL should not have been added" ); + } + + private string function returnTierDev() { + return "dev"; + } + + public void function testTierDevOne() { + variables.fw.getEnvironment = returnTierDevOne; + variables.fwvars.getEnvironment = returnTierDevOne; + variables.fw.onRequestStart( "" ); + assertTrue( variables.fwcfg.reloadApplicationOnEveryRequest, "Reload should be dev: true" ); + assertTrue( structKeyExists( variables.fwcfg, "oneNewOption" ), "OneNewOption should be present" ); + assertFalse( structKeyExists( variables.fwcfg, "useSSL" ), "UseSSL should not have been added" ); + } + + private string function returnTierDevOne() { + return "dev-one"; + } + + public void function testTierDevTwo() { + variables.fw.getEnvironment = returnTierDevTwo; + variables.fwvars.getEnvironment = returnTierDevTwo; + variables.fw.onRequestStart( "" ); + assertFalse( variables.fwcfg.reloadApplicationOnEveryRequest, "Reload should be dev-one: false (dev-two overrides dev)" ); + assertFalse( structKeyExists( variables.fwcfg, "oneNewOption" ), "OneNewOption should not have been added" ); + assertFalse( structKeyExists( variables.fwcfg, "useSSL" ), "UseSSL should not have been added" ); + } + + private string function returnTierDevTwo() { + return "dev-two"; + } + + public void function testTierDevNone() { + variables.fw.getEnvironment = returnTierDevNone; + variables.fwvars.getEnvironment = returnTierDevNone; + variables.fw.onRequestStart( "" ); + assertTrue( variables.fwcfg.reloadApplicationOnEveryRequest, "Reload should be dev: true (dev-none introduces no override)" ); + assertFalse( structKeyExists( variables.fwcfg, "oneNewOption" ), "OneNewOption should not have been added" ); + assertFalse( structKeyExists( variables.fwcfg, "useSSL" ), "UseSSL should not have been added" ); + } + + private string function returnTierDevNone() { + return "dev-none"; + } + + public void function testTierProdOnly() { + variables.fw.getEnvironment = returnTierProd; + variables.fwvars.getEnvironment = returnTierProd; + variables.fw.onRequestStart( "" ); + assertFalse( variables.fwcfg.reloadApplicationOnEveryRequest, "Reload should be prod: false" ); + assertFalse( structKeyExists( variables.fwcfg, "oneNewOption" ), "OneNewOption should not have been added" ); + assertTrue( structKeyExists( variables.fwcfg, "useSSL" ), "UseSSL should be present" ); + assertTrue( variables.fwcfg.useSSL, "UseSSL should be true" ); + } + + private string function returnTierProd() { + return "prod"; + } + + public void function testTierProdPlus() { + variables.fw.getEnvironment = returnTierProdPlus; + variables.fwvars.getEnvironment = returnTierProdPlus; + variables.fw.onRequestStart( "" ); + assertFalse( variables.fwcfg.reloadApplicationOnEveryRequest, "Reload should be prod: false" ); + assertFalse( structKeyExists( variables.fwcfg, "oneNewOption" ), "OneNewOption should not have been added" ); + assertTrue( structKeyExists( variables.fwcfg, "useSSL" ), "UseSSL should be present" ); + assertTrue( variables.fwcfg.useSSL, "UseSSL should be true" ); + } + + private string function returnTierProdPlus() { + return "prod-plus"; + } + + public void function testHostname() { + // just tests we get a non-empty string + assertNotEquals( "", variables.fw.getHostname() ); + } + +} diff --git a/tests/frameworkErrorTest.cfc b/tests/frameworkErrorTest.cfc new file mode 100644 index 00000000..da41713c --- /dev/null +++ b/tests/frameworkErrorTest.cfc @@ -0,0 +1,52 @@ +component extends="mxunit.framework.TestCase" { + + public void function setUp() { + variables.fw = new org.corfield.framework(); + request.failureCount = 0; + request.outputContent = ""; + injectMethod(variables.fw, this, "exceptionCapture", "dumpException"); + } + + /** + * Test with initialised framework - ensure error handler tries to render the main.error view + */ + public void function testError() + { + var exception = { + type = "Testing", + message = "Testing", + detail = "Detail" + }; + var event = "Test Event"; + variables.fw.onApplicationStart(); + savecontent variable="output" { + variables.fw.onError(exception, event); + }; + assertEquals(request.action, "main.error"); + assertTrue(output contains "Unable to find a view for 'main.error' action."); + } + + /** + * Test with un-initialised framework - ensure internal error is not as prominent + */ + public void function testEarlyError() + { + var exception = { + type = "Testing", + message = "Testing", + detail = "Detail" + }; + var event = ""; + savecontent variable="output" { + variables.fw.onError(exception, event); + } + assertFalse(output CONTAINS "Element FRAMEWORK.USINGSUBSYSTEMS is undefined in VARIABLES", "Didn't expect failure in early exception"); + assertTrue(output CONTAINS "Exception occured before FW/1 was initialized", "Expected message about early exception"); + } + + private void function exceptionCapture( any exception) + { + writeLog(text="Exception: #exception.message#"); + request.capturedException = arguments.exception; + } +} \ No newline at end of file diff --git a/tests/frameworkPopulateTest.cfc b/tests/frameworkPopulateTest.cfc new file mode 100644 index 00000000..aa4917c3 --- /dev/null +++ b/tests/frameworkPopulateTest.cfc @@ -0,0 +1,164 @@ +component extends="mxunit.framework.TestCase" { + + public void function setUp() { + variables.fw = new org.corfield.framework(); + clearFW1MetaData(); + } + + public void function testPopulateFlatComponent() { + var user = new stubs.userOneLevel(); + request.context = getOneLevelRC(); + + variables.fw.populate( user ); + + assertEquals( request.context.username,user.getUserName() ); + assertEquals( request.context.firstName,user.getFirstName() ); + assertEquals( request.context.lastName,user.getLastName() ); + assertEquals( request.context.isActive,user.getIsActive() ); + } + + public void function testPopulateFlatComponentWithKeys() { + var user = new stubs.userOneLevel(); + request.context = getOneLevelRC(); + + variables.fw.populate( cfc=user, keys="username,firstname", deep=true ); + + assertEquals( request.context.username, user.getUserName() ); + assertEquals( request.context.firstName, user.getFirstName() ); + assertEquals( "", user.getLastName() ); + assertEquals( false, user.getIsActive() ); + } + + public void function testPopulateChildComponentWithKeys() { + var user = new stubs.userTwoLevel(); + request.context = getTwoLevelRC(); + + variables.fw.populate( cfc=user, keys="contact.firstName,username", deep=true ); + + assertEquals( request.context.username, user.getUserName() ); + assertEquals( request.context[ "contact.firstName" ], user.getContact().getFirstName() ); + assertEquals( "", user.getContact().getLastName() ); + } + + public void function testPopulateChildComponentWithTrustKeys() { + var user = new stubs.userTwoLevel(); + request.context = getTwoLevelRC(); + + variables.fw.populate( cfc=user, trustKeys=true ); + + assertEquals( request.context.username,user.getUserName() ); + assertEquals( request.context[ "contact.firstName" ], user.getContact().getFirstName() ); + assertEquals( request.context[ "contact.lastName" ], user.getContact().getLastName() ); + } + + public void function testComponentWithSingleChild() { + var user = new stubs.userTwoLevel(); + request.context = getTwoLevelRC(); + + variables.fw.populate( cfc=user, deep=true ); + + assertEquals( request.context.username,user.getUserName() ); + assertEquals( request.context[ "contact.firstName" ], user.getContact().getFirstName() ); + assertEquals( request.context[ "contact.lastName" ], user.getContact().getLastName() ); + assertEquals( request.context[ "contact.dateCreated" ], user.getContact().getDateCreated() ); + } + + public void function testComponentWithSingleChildAndDeepFalse() { + var user = new stubs.userTwoLevel(); + request.context = getTwoLevelRC(); + + variables.fw.populate( cfc=user ); + + assertEquals( request.context.username,user.getUserName() ); + assertEquals( "",user.getContact().getFirstName() ); + assertEquals( "",user.getContact().getLastName() ); + assertEquals( true,user.getIsActive() ); + } + + public void function testComponentWithManyChildren() { + var user = new stubs.userThreeLevel(); + request.context = getThreeLevelRC(); + + variables.fw.populate(cfc=user,deep=true); + + assertEquals( request.context.username, user.getUserName() ); + assertEquals( request.context[ "contact.firstName" ], user.getContact().getFirstName() ); + assertEquals( request.context[ "contact.lastName" ], user.getContact().getLastName() ); + assertEquals( request.context.isActive, user.getIsActive() ); + assertEquals( request.context[ "contact.address.line1" ], user.getContact().getAddress().GetLine1() ); + assertEquals( request.context[ "contact.address.line2" ], user.getContact().getAddress().GetLine2() ); + assertEquals( request.context[ "contact.address.zipCode" ], user.getContact().getAddress().GetZipCode() ); + } + + public void function testComponentWithManyChildrenAndTrustKeys() { + var user = new stubs.userThreeLevel(); + request.context = getThreeLevelRC(); + + variables.fw.populate( cfc=user, deep=true, trustKeys=true ); + + assertEquals( request.context.username, user.getUserName() ); + assertEquals( request.context[ "contact.firstName"], user.getContact().getFirstName() ); + assertEquals( request.context[ "contact.lastName"], user.getContact().getLastName() ); + assertEquals( request.context.isActive, user.getIsActive() ); + assertEquals( request.context[ "contact.address.line1"], user.getContact().getAddress().GetLine1() ); + assertEquals( request.context[ "contact.address.line2"], user.getContact().getAddress().GetLine2() ); + assertEquals( request.context[ "contact.address.zipCode"], user.getContact().getAddress().GetZipCode() ); + } + + public void function testComponentWithManyChildrenPassInKeys() { + var user = new stubs.userThreeLevel(); + request.context = getThreeLevelRC(); + + variables.fw.populate( cfc=user, deep=true, keys = "contact.firstName,contact.address.line1,username" ); + + assertEquals( request.context.username, user.getUserName() ); + assertEquals( request.context[ "contact.firstName" ], user.getContact().getFirstName() ); + assertEquals( "", user.getContact().getLastName() ); + assertEquals( false, user.getIsActive() ); + assertEquals( request.context[ "contact.address.line1" ], user.getContact().getAddress().GetLine1() ); + assertEquals( "", user.getContact().getAddress().GetLine2() ); + assertEquals( "", user.getContact().getAddress().GetZipCode() ); + } + + private Struct function getOneLevelRC() + output=false { + return { username = "foobar", firstName="Homer", lastName="Simpson", isActive=true }; + } + + private Struct function getTwoLevelRC() + output=false { + return { username = "foobar", "contact.firstName" = "Homer", "contact.lastName" = "Simpson", isActive = true, "contact.dateCreated" = "02/29/2012" }; + } + + private Struct function getThreeLevelRC() + output=false { + return { + username = "foobar", + "contact.firstName" = "Homer", + "contact.lastName" = "Simpson", + "contact.dateCreated" = "02/29/2012", + isActive = true, + "contact.address.line1" = "123 Fake Street", + "contact.address.line2" = "Apt 12", + "contact.address.zipCode" = "54232" + }; + } + + private void function clearFW1MetaData() + output=false hint=""{ + var cfcs = {}; + + cfcs[ "stubs.Address" ] = getMetaData( new stubs.Address() ); + + cfcs[ "stubs.Contact" ] = getMetaData( new stubs.Contact() ); + cfcs[ "stubs.UserOneLevel" ] = getMetaData( new stubs.UserOneLevel() ); + cfcs[ "stubs.UserTwoLevel" ] = getMetaData( new stubs.UserTwoLevel() ); + cfcs[ "stubs.UserThreeLevel" ] = getMetaData( new stubs.UserThreeLevel() ); + + for(cfc in cfcs){ + if ( structKeyExists( cfcs[ cfc ], '__fw1_setters' ) ) { + structDelete( cfcs[ cfc ], "__fw1_setters" ); + } + } + } +} \ No newline at end of file diff --git a/tests/frameworkRouteTest.cfc b/tests/frameworkRouteTest.cfc new file mode 100644 index 00000000..053dab6e --- /dev/null +++ b/tests/frameworkRouteTest.cfc @@ -0,0 +1,85 @@ +component extends="tests.InjectableTest" { + + public void function setUp() { + variables.fw = new org.corfield.framework(); + // doesn't work on Railo: + // makePublic(variables.fw, "processRouteMatch"); + // this works on both Railo and ACF: + variables.fw.processRouteMatch = getVariablesScope(variables.fw).processRouteMatch; + } + + public void function testRouteMatchBasics() + { + var match = variables.fw.processRouteMatch("/test", "routed", "/test"); + assertTrue(match.matched); + assertEquals("/test/(.*)", match.pattern); + assertEquals("routed/\1", match.target); + + match = variables.fw.processRouteMatch("/test2/:id", "default.main?id=:id", "/test2/5"); + assertTrue(match.matched); + assertEquals("/test2/([^/]*)/(.*)", match.pattern); + assertEquals("default.main?id=\1/\2", match.target); + + match = variables.fw.processRouteMatch("/test2/:id", "default.main?id=:id", "/test2"); + assertFalse(match.matched); + + match = variables.fw.processRouteMatch("/test/:foo/bar/:baz", "default.main?foo=:foo&baz=:baz", "/test/quux/bar/fnarf"); + assertTrue(match.matched); + assertEquals("/test/([^/]*)/bar/([^/]*)/(.*)", match.pattern); + assertEquals("default.main?foo=\1&baz=\2/\3", match.target); + } + + public void function testRouteMatchRegex() + { + match = variables.fw.processRouteMatch("/test2/:id", "default.main?id=:id", "/test2/5/people"); + assertTrue(match.matched); + + match = variables.fw.processRouteMatch("/(blog|forum|forums)/:action/", "/forum::action/", "/blog/post"); + assertTrue(match.matched); + assertEquals("/forum:post/", rereplace( match.path, match.pattern, match.target )); + + match = variables.fw.processRouteMatch("/test2/:id/", "default.main?id=:id", "/test2/5/people"); + assertTrue(match.matched, "/test2/:id should match /test2/5/people"); + + match = variables.fw.processRouteMatch("/test2/:id/$", "default.main?id=:id", "/test2/5/people"); + assertFalse(match.matched, "/test2/:id/$ shouldn't match /test2/5/people"); + + match = variables.fw.processRouteMatch("/test2/(\d+)/$", "default.main/id/\1/", "/test2/5"); + assertTrue(match.matched); + assertEquals("default.main/id/5/", rereplace(match.path, match.pattern, match.target)); + + match = variables.fw.processRouteMatch("/test2/(\d+)/$", "default.main/id/\1/", "/test2/zz/"); + assertFalse(match.matched); + + var route = "/test/(\d+)/something(\.)?(\w+)?/$"; + var target = "default.main/id/\1/type/\3/"; + match = variables.fw.processRouteMatch(route, target, "/test/5/something/"); + assertTrue(match.matched, "/test/5/something/ should match"); + assertEquals("default.main/id/5/type//", rereplace(match.path, match.pattern, match.target)); + + match = variables.fw.processRouteMatch(route, target, "/test/5/something.html/"); + assertTrue(match.matched); + assertEquals("default.main/id/5/type/html/", rereplace(match.path, match.pattern, match.target)); + } + + public void function testRouteMatchMethod() + { + match = variables.fw.processRouteMatch("$GET/test/:id", "default.main?id=:id", "/test/5"); + assertTrue(match.matched); + + match = variables.fw.processRouteMatch("$POST/test/:id", "default.main?id=:id", "/test/5"); + assertFalse(match.matched); + } + + public void function testRouteMatchRedirect() + { + match = variables.fw.processRouteMatch("/test/:id", "default.main?id=:id", "/test/5"); + assertTrue(match.matched); + assertFalse(match.redirect); + + match = variables.fw.processRouteMatch("/test/:id", "302:default.main?id=:id", "/test/5"); + assertTrue(match.matched); + assertTrue(match.redirect); + assertEquals(302, match.statusCode); + } +} diff --git a/tests/frameworkTestSuite.cfm b/tests/frameworkTestSuite.cfm new file mode 100644 index 00000000..69e96025 --- /dev/null +++ b/tests/frameworkTestSuite.cfm @@ -0,0 +1,11 @@ + +testSuite = createObject("component","mxunit.framework.TestSuite").TestSuite(); +testSuite.addAll("tests.frameworkPopulateTest"); +testSuite.addAll("tests.frameworkErrorTest"); +testSuite.addAll("tests.frameworkRouteTest"); +testSuite.addAll("tests.frameworkEnvTest"); +testSuite.addAll("tests.onMissingViewLayout"); +testSuite.addAll("tests.onSessionStartBuildURL"); +results = testSuite.run(); +writeOutput(results.getResultsOutput('html')); + diff --git a/tests/omv/layouts/main/default.cfm b/tests/omv/layouts/main/default.cfm new file mode 100644 index 00000000..3cc31863 --- /dev/null +++ b/tests/omv/layouts/main/default.cfm @@ -0,0 +1 @@ +DEFAULT#body# diff --git a/tests/omv/layouts/main/two.cfm b/tests/omv/layouts/main/two.cfm new file mode 100644 index 00000000..35e390d3 --- /dev/null +++ b/tests/omv/layouts/main/two.cfm @@ -0,0 +1 @@ +TWO#body# diff --git a/tests/omv/views/main/test.cfm b/tests/omv/views/main/test.cfm new file mode 100644 index 00000000..2a02d41c --- /dev/null +++ b/tests/omv/views/main/test.cfm @@ -0,0 +1 @@ +TEST diff --git a/tests/onMissingViewLayout.cfc b/tests/onMissingViewLayout.cfc new file mode 100644 index 00000000..ae350026 --- /dev/null +++ b/tests/onMissingViewLayout.cfc @@ -0,0 +1,29 @@ +component extends="tests.InjectableTest" { + + public void function setUp() { + structClear( request ); + variables.fw = new org.corfield.framework(); + variables.fwvars = getVariablesScope( variables.fw ); + variables.fwvars.framework = { + base = "/tests/omv" + }; + } + + public void function testSetLayout() { + // should be able to run setLayout() in onMissingView() and have it choose the layout + variables.fw.onMissingView = selectLayoutTwo; + variables.fwvars.onMissingView = selectLayoutTwo; + variables.fw.onRequestStart( "" ); + var output = ""; + savecontent variable="output" { + variables.fw.onRequest( "" ); + } + assertEquals( "TWOTEST", trim( output ) ); + } + + private string function selectLayoutTwo( struct rc ) { + setLayout( "main.two" ); + return view( "main/test" ); + } + +} diff --git a/tests/onSessionStartBuildURL.cfc b/tests/onSessionStartBuildURL.cfc new file mode 100644 index 00000000..600e387a --- /dev/null +++ b/tests/onSessionStartBuildURL.cfc @@ -0,0 +1,27 @@ +component extends="tests.InjectableTest" { + + public void function setUp() { + structClear( request ); + variables.fw = new org.corfield.framework(); + variables.fwvars = getVariablesScope( variables.fw ); + variables.fwvars.framework = { + generateSES = true, + SESOmitIndex = true + }; + } + + public void function testBuildURL() { + // ensure SES URL gets generated in onSessionStart: + variables.fw.setupSession = buildSESURL; + variables.fwvars.setupSession = buildSESURL; + variables.fw.__url = ""; + variables.fw.onSessionStart(); + var expected = "/foo/test/bar/1"; + assertEquals( expected, right( variables.fw.__url, len( expected ) ) ); + } + + private void function buildSESURL() { + this.__url = buildURL( action = "foo.test", queryString = "bar=1" ); + } + +} diff --git a/tests/stubs/Address.cfc b/tests/stubs/Address.cfc new file mode 100644 index 00000000..1545c194 --- /dev/null +++ b/tests/stubs/Address.cfc @@ -0,0 +1,15 @@ +component accessors = true{ + + property name = line1 getters = true setters = true type = string; + + property name = line2 getters = true setters = true type = string; + + property name = zipCode getters = true setters = true type = string; + + public void function init() + output=false hint="constructor"{ + variables.line1 = ""; + variables.line2 = ""; + variables.zipCode = ""; + } +} \ No newline at end of file diff --git a/tests/stubs/Contact.cfc b/tests/stubs/Contact.cfc new file mode 100644 index 00000000..b2243c00 --- /dev/null +++ b/tests/stubs/Contact.cfc @@ -0,0 +1,16 @@ +component accessors = true { + + property name = firstName getters = true setters = true type = string; + property name = lastName getters = true setters = true type = string; + property name = dateCreated getters = true setters = true type = date; + property name = address getters = true setters = true type = stubs.Address; + + public void function init() + output = false hint = "constructor" { + variables.firstName = ""; + variables.lastName = ""; + + variables.Address = new Address(); + //intentionally not initing date created + } +} \ No newline at end of file diff --git a/tests/stubs/UserOneLevel.cfc b/tests/stubs/UserOneLevel.cfc new file mode 100644 index 00000000..cf5e800c --- /dev/null +++ b/tests/stubs/UserOneLevel.cfc @@ -0,0 +1,16 @@ +component accessors = true { + + + property name = username getters = true setters = true type = string; + property name = firstName getters = true setters = true type = string; + property name = lastName getters = true setters = true type = string; + property name = isActive getters = true setters = true type = boolean; + + public void function init() + output = false hint = "constructor" { + variables.username = ""; + variables.firstName = ""; + variables.lastName = ""; + variables.isActive = false; + } +} \ No newline at end of file diff --git a/tests/stubs/UserThreeLevel.cfc b/tests/stubs/UserThreeLevel.cfc new file mode 100644 index 00000000..c44c53d5 --- /dev/null +++ b/tests/stubs/UserThreeLevel.cfc @@ -0,0 +1,14 @@ +component accessors = true { + + property name = username getters = true setters = true type = string; + property name = isActive getters = true setters = true type = boolean; + property name = contact getters = true setters = true type = stubs.Contact; + + + public void function init() + output = false hint = "constructor" { + variables.username = ""; + variables.Contact = new Contact(); + variables.isActive = false; + } +} \ No newline at end of file diff --git a/tests/stubs/UserTwoLevel.cfc b/tests/stubs/UserTwoLevel.cfc new file mode 100644 index 00000000..3bf0b425 --- /dev/null +++ b/tests/stubs/UserTwoLevel.cfc @@ -0,0 +1,13 @@ +component accessors = true { + + property name = username getters = true setters = true type = string; + property name = contact getters = true setters = true type = stubs.Contact; + property name = isActive getters = true setters = true type = boolean; + + + public void function init() + output = false hint = "constructor" { + variables.username = ""; + variables.Contact = new Contact(); + } +} \ No newline at end of file