diff --git a/.gitignore b/.gitignore index 61cc0f2d..69569d9c 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ settings.xml run-tests.sh /examples/6helloclojure/target/stale/extract-native.dependencies /taskmanager/ +/server.json diff --git a/.travis.yml b/.travis.yml index 5183710a..e5abee62 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,6 @@ language: java env: matrix: - PLATFORM=acf10-linux64 - - PLATFORM=acf902-linux64 - PLATFORM=railo42beta - PLATFORM=railo41 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5bc7ea2a..cf78256b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,6 +10,8 @@ Look at `run-tests-example.sh` to see how to run tests locally (copy that shell Please follow the same formatting as the existing code, especially in terms of spacing around operators, parentheses, braces and so on. If in doubt, ask on the mailing list. +By submitting a Pull Request, you are granting copyright license to Sean Corfield and that your submission may be legally released under the Apache Source License 2.0 (http://www.apache.org/licenses/LICENSE-2.0). + The **master** branch represents the current stable release of FW/1. Do not submit Pull Requests against **master**. Showstopping bugs should be raised as issues and fixes will be applied to **develop** (if appropriate) and backported to **master** manually. **Note:** Do not submit Pull Requests against [Sean's personal fork](https://github.com/seancorfield/fw1) - that exists for historical reasons and Github doesn't let you turn Pull Requests off. diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..210b6d6f --- /dev/null +++ b/LICENSE @@ -0,0 +1,14 @@ +Copyright (c) 2009-2016 Sean Corfield (see individual files for any + additional copyright holders) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README.md b/README.md index 03556668..c46a3c73 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# FW/1 (Framework One) [![Build Status](https://travis-ci.org/framework-one/fw1.png)](https://travis-ci.org/framework-one/fw1) [![Join the chat at https://gitter.im/framework-one/fw1](https://badges.gitter.im/framework-one/fw1.svg)](https://gitter.im/framework-one/fw1?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +# FW/1 (Framework One) [![Build Status](https://travis-ci.org/framework-one/fw1.png)](https://travis-ci.org/framework-one/fw1) [![Stories in Ready](https://badge.waffle.io/framework-one/fw1.png?label=ready&title=Ready)](http://waffle.io/framework-one/fw1) [![Join the chat at https://gitter.im/framework-one/fw1](https://badges.gitter.im/framework-one/fw1.svg)](https://gitter.im/framework-one/fw1?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 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 @@ -9,7 +9,9 @@ in your admin - you can't just use a per-application mapping. Please read the [Framework One Code of Conduct](CODE_OF_CONDUCT.md) - we want FW/1 to be a welcoming and supportive environment for everyone to feel comfortable contributing! -**Project home:** http://fw1.riaforge.org +# Resources + +**Project home:** https://github.com/framework-one/fw1 **Documentation / Wiki:** http://framework-one.github.io/documentation/ / http://github.com/framework-one/fw1/wiki @@ -19,7 +21,7 @@ Please read the [Framework One Code of Conduct](CODE_OF_CONDUCT.md) - we want FW **Chat:** The [CFML team Slack](http://cfml-slack.herokuapp.com) has a [dedicated #fw1 channel](https://cfml.slack.com/messages/fw1/). -**Running the tests:** +# Running the Tests The Ant `build.xml` file is primarily designed to be used by Travis to run the tests automatically, but it is possible to run the tests locally, with some setup: @@ -40,3 +42,9 @@ See the `run-tests-example.sh` file for a template (for Mac/Linux). * `server.name` should be the test domain you have configured * `server.port` should be the port on which you access that test domain * `run-tests-mxunit` is the actual Ant task that does the testing + +# Copyright and License + +Copyright (c) 2009-2016 Sean Corfield (and others -- see individual files for additional copyright holders). All rights reserved. +The use and distribution terms for this software are covered by the Apache Software License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) which can also be found in the file LICENSE at the root of this distribution and in individual licensed files. +By using this software in any fashion, you are agreeing to be bound by the terms of this license. You must not remove this notice, or any other, from this software. diff --git a/box.json b/box.json index 37811d60..7376679b 100644 --- a/box.json +++ b/box.json @@ -1,9 +1,9 @@ { "name" : "Framework One", "slug" : "fw1", - "version" : "3.5.1", + "version" : "4.0.0", "author" : "Sean Corfield, Marcin Szczepanski, Ryan Cogswell", - "location" : "https://github.com/framework-one/fw1/archive/v3.5.1.zip", + "location" : "https://github.com/framework-one/fw1/archive/4.0.0.zip", "createPackageDirectory" : true, "packageDirectory" : "framework", "Homepage" : "http://framework-one.github.io/", @@ -22,7 +22,7 @@ "engines" : [ { "type" : "railo", "version" : ">=4.1.x" }, { "type" : "lucee", "version" : ">=4.5.x" }, - { "type" : "adobe", "version" : ">=9.0.2" } + { "type" : "adobe", "version" : ">=10.0.x" } ], "License" : [ { "type" : "Apache 2.0", "URL" : "http://www.apache.org/licenses/LICENSE-2.0" } diff --git a/build.xml b/build.xml index c7584276..a6503415 100644 --- a/build.xml +++ b/build.xml @@ -21,7 +21,6 @@ - @@ -31,7 +30,6 @@ - @@ -42,7 +40,6 @@ - Unkown platform ${platform} for source ${source}. @@ -52,8 +49,7 @@ Valid values are: railo41-osx railo42beta acf10-linux32 - acf10-linux64 - acf902-linux64 + acf10-linux64 diff --git a/examples/layouts/default.cfm b/examples/layouts/default.cfm index c3658ca3..d0f85065 100644 --- a/examples/layouts/default.cfm +++ b/examples/layouts/default.cfm @@ -12,7 +12,7 @@

Examples Home

diff --git a/examples/subsystems/4hellocontroller/views/main/default.cfm b/examples/subsystems/4hellocontroller/views/main/default.cfm index 0a8704d9..d61f858e 100644 --- a/examples/subsystems/4hellocontroller/views/main/default.cfm +++ b/examples/subsystems/4hellocontroller/views/main/default.cfm @@ -1,4 +1,4 @@

Hello #rc.name#!

-

Go away!

+

Go away!

diff --git a/examples/subsystems/5helloservice/views/main/default.cfm b/examples/subsystems/5helloservice/views/main/default.cfm index 0a8704d9..d61f858e 100644 --- a/examples/subsystems/5helloservice/views/main/default.cfm +++ b/examples/subsystems/5helloservice/views/main/default.cfm @@ -1,4 +1,4 @@

Hello #rc.name#!

-

Go away!

+

Go away!

diff --git a/examples/subsystems/6helloclojure/boot.properties b/examples/subsystems/6helloclojure/boot.properties new file mode 100644 index 00000000..421b1965 --- /dev/null +++ b/examples/subsystems/6helloclojure/boot.properties @@ -0,0 +1,6 @@ +#http://boot-clj.com +#Mon Dec 21 11:44:14 PST 2015 +BOOT_CLOJURE_NAME=org.clojure/clojure +BOOT_CLOJURE_VERSION=1.7.0 +BOOT_VERSION=2.5.2 +BOOT_EMIT_TARGET=no diff --git a/examples/subsystems/6helloclojure/build.boot b/examples/subsystems/6helloclojure/build.boot new file mode 100644 index 00000000..29581d13 --- /dev/null +++ b/examples/subsystems/6helloclojure/build.boot @@ -0,0 +1,9 @@ +;; very minimal Boot build file: for use with cfmljure we do not +;; need any pom / jar information, just the dependencies and a +;; matching boot.properties file +(set-env! :resource-paths #{"src"} + :source-paths #{"test"} + :dependencies '[[org.clojure/clojure "1.7.0"] + [adzerk/boot-test "1.0.7"]]) + +(require '[adzerk.boot-test :refer [test]]) diff --git a/framework/Application.cfc b/framework/Application.cfc index f41b4560..28b16a26 100644 --- a/framework/Application.cfc +++ b/framework/Application.cfc @@ -1,5 +1,6 @@ component { - // Version: FW/1 3.5.1 + // Version: FW/1 4.0.0 + // copy this to your application root to use as your Application.cfc // or incorporate the logic below into your existing Application.cfc diff --git a/framework/MyApplication.cfc b/framework/MyApplication.cfc index e65721ad..e4da02a5 100644 --- a/framework/MyApplication.cfc +++ b/framework/MyApplication.cfc @@ -1,5 +1,5 @@ component extends="framework.one" { - // Version: FW/1 3.5.1 + // Version: FW/1 4.0.0 // if you need to provide extension points, copy this to // your web root, next to your Application.cfc, and add diff --git a/framework/WireBoxAdapter.cfc b/framework/WireBoxAdapter.cfc index 3aa7a018..02240f58 100644 --- a/framework/WireBoxAdapter.cfc +++ b/framework/WireBoxAdapter.cfc @@ -1,7 +1,7 @@ component extends="wirebox.system.ioc.Injector" { - variables._fw1_version = "3.5.1"; + variables._fw1_version = "4.0.0"; /* - Copyright (c) 2010-2015, Sean Corfield + Copyright (c) 2010-2016, Sean Corfield Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/framework/aop.cfc b/framework/aop.cfc old mode 100755 new mode 100644 index 7d44795e..c80a60ce --- a/framework/aop.cfc +++ b/framework/aop.cfc @@ -1,312 +1,312 @@ -component extends="framework.ioc" { - variables._fw1_version = "3.5.1"; - variables._aop1_version = "2.0.1"; -/* - Copyright (c) 2013-2015, Mark Drew, Sean Corfield, Daniel Budde - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - // Internal cache of interceptor definitions. - variables.interceptorCache = {regex = [], name = {}, type = []}; - - - - - // -------------- // - // PUBLIC METHODS // - // -------------- // - - /** Constructor. */ - public any function init(string folders, struct config = {}) - { - super.init(argumentCollection = arguments); - - if (structKeyExists(arguments.config, "interceptors") && isArray(arguments.config.interceptors) && arrayLen(arguments.config.interceptors)) - { - loadInterceptors(arguments.config.interceptors); - } - } - - - /** Adds an interceptor definition to the definition cache. */ - public any function intercept(string beanName, string interceptorName, string methods = "") - { - var interceptDefinition = - { - name = arguments.interceptorName, - methods = arguments.methods - }; - - - arguments.beanName = trim(arguments.beanName); - - - // Determine if this is a name match or regex match. - if (len(arguments.beanName) && left(arguments.beanName, 1) == "/" && right(arguments.beanName, 1) == "/") - { - // Store regex without the forward slashes. - interceptDefinition.regex = mid(arguments.beanName, 2, len(arguments.beanName) - 2); - - arrayAppend(variables.interceptorCache.regex, interceptDefinition); - } - else - { - if (!structKeyExists(variables.interceptorCache.name, arguments.beanName)) - { - variables.interceptorCache.name[arguments.beanName] = []; - } - - arrayAppend(variables.interceptorCache.name[arguments.beanName], interceptDefinition); - } - - - return this; - } - - - /** Adds an interceptor definition to the definition cache. */ - public any function interceptByType(string type, string interceptorName, string methods = "") - { - var interceptDefinition = - { - type = arguments.type, - name = arguments.interceptorName, - methods = arguments.methods - }; - - arrayAppend(variables.interceptorCache.type, interceptDefinition); - } - - - - - // --------------- // - // PRIVATE METHODS // - // --------------- // - - /** Hook point to wrap bean with proxy. */ - private any function construct(string dottedPath) - { - var bean = super.construct(arguments.dottedPath); - var beanProxy = ""; - - // if it doesn't have a dotted path for us to create a new instance - // or it has no interceptors, we have to leave it alone - if (!hasInterceptors(arguments.dottedPath)) - { - return bean; - } - - // Create and return a proxy wrapping the bean. - beanProxy = new framework.beanProxy(bean, getInterceptorsForBean(arguments.dottedPath), variables.config); - - return beanProxy; - } - - - /** Gets the associated interceptor definitions for a specific bean. */ - private array function getInterceptorsForBean(string dottedPath) - { - // build the interceptor array: - var beanName = listLast(arguments.dottedPath, "."); - var beanNames = getAliases(beanName); - var beanTypes = ""; - var interceptDefinition = ""; - var interceptedBeanName = ""; - var interceptors = []; - - - arrayPrepend(beanNames, beanName); - - - // Grab all name based interceptors that match. - for (interceptedBeanName in beanNames) - { - // Match on name. - if (structKeyExists(variables.interceptorCache.name, interceptedBeanName)) - { - for (interceptDefinition in variables.interceptorCache.name[interceptedBeanName]) - { - arrayAppend(interceptors, {bean = getBean(interceptDefinition.name), methods = interceptDefinition.methods}); - } - } - } - - - // Match on regex. Ensure we only attach each one time. - if (arrayLen(variables.interceptorCache.regex)) - { - for (interceptDefinition in variables.interceptorCache.regex) - { - for (interceptedBeanName in beanNames) - { - if (reFindNoCase(interceptDefinition.regex, interceptedBeanName)) - { - arrayAppend(interceptors, {bean = getBean(interceptDefinition.name), methods = interceptDefinition.methods}); - break; - } - } - } - } - - - // Grab all type based interceptors that match. - if (arrayLen(variables.interceptorCache.type)) - { - beanTypes = getBeanTypes(arguments.dottedPath); - - for (interceptDefinition in variables.interceptorCache.type) - { - if (listFindNoCase(beanTypes, interceptDefinition.type)) - { - arrayAppend(interceptors, {bean = getBean(interceptDefinition.name), methods = interceptDefinition.methods}); - } - } - } - - - return interceptors; - } - - - /** Determines if the bean has interceptor definitions associated with it. */ - private boolean function hasInterceptors(string dottedPath) - { - var interceptedBeanName = ""; - var interceptorDefinition = {}; - var beanName = listLast(arguments.dottedPath, "."); - var beanNames = getAliases(beanName); - var beanTypes = ""; - - - arrayPrepend(beanNames, beanName); - - - for (interceptedBeanName in beanNames) - { - // Look for matches on name first. - if (structKeyExists(variables.interceptorCache.name, interceptedBeanName)) - { - return true; - } - - - // Look for matches on regex. - if (arrayLen(variables.interceptorCache.regex)) - { - for (interceptorDefinition in variables.interceptorCache.regex) - { - if (reFindNoCase(interceptorDefinition.regex, interceptedBeanName)) - { - return true; - } - } - } - - - // Look for matches by bean type. - if (arrayLen(variables.interceptorCache.type)) - { - beanTypes = getBeanTypes(arguments.dottedPath); - - for (interceptorDefinition in variables.interceptorCache.type) - { - if (listFindNoCase(beanTypes, interceptorDefinition.type)) - { - return true; - } - } - } - } - - - return false; - } - - - /** Finds all aliases for the given beanName. */ - private array function getAliases(string beanName) - { - var aliases = []; - var beanData = ""; - var key = ""; - - - if (structKeyExists(variables.beanInfo, arguments.beanName)) - { - beanData = variables.beanInfo[arguments.beanName]; - - for (key in variables.beanInfo) - { - // Same cfc dotted path, must be an alias. - if ( - key != arguments.beanName && - structKeyExists(variables.beanInfo[key], "cfc") && - structKeyExists(variables.beanInfo[arguments.beanName], "cfc") && - variables.beanInfo[key].cfc == variables.beanInfo[arguments.beanName].cfc) - { - arrayAppend(aliases, key); - } - } - } - - return aliases; - } - - - /** Returns a list of bean types (both name and dotted path) for a given bean. */ - private string function getBeanTypes(string dottedPath) - { - var beanTypes = ""; - var metadata = getComponentMetadata(arguments.dottedPath); - - while (!len(beanTypes) || structKeyExists(metadata, "extends")) - { - beanTypes = listAppend(beanTypes, listLast(metadata.name, ".")); - beanTypes = listAppend(beanTypes, metadata.name); - - if (structKeyExists(metadata, "extends")) - { - metadata = metadata.extends; - } - } - - return beanTypes; - } - - - /** Loads an array of interceptor definitions into the interceptor definition cache. */ - private void function loadInterceptors(array interceptors) - { - var interceptor = false; - - for (interceptor in interceptors) - { - if (structKeyExists(interceptor, "beanName")) - { - intercept(argumentCollection = interceptor); - } - else - { - interceptByType(argumentCollection = interceptor); - } - } - } - - - private void function setupFrameworkDefaults() - { - super.setupFrameworkDefaults(); - variables.config.version = variables._aop1_version & " (" & variables._di1_version & ")"; - } -} +component extends="framework.ioc" { + variables._fw1_version = "4.0.0"; + variables._aop1_version = "2.0.2"; +/* + Copyright (c) 2013-2016, Mark Drew, Sean Corfield, Daniel Budde + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + // Internal cache of interceptor definitions. + variables.interceptorCache = {regex = [], name = {}, type = []}; + + + + + // -------------- // + // PUBLIC METHODS // + // -------------- // + + /** Constructor. */ + public any function init(string folders, struct config = {}) + { + super.init(argumentCollection = arguments); + + if (structKeyExists(arguments.config, "interceptors") && isArray(arguments.config.interceptors) && arrayLen(arguments.config.interceptors)) + { + loadInterceptors(arguments.config.interceptors); + } + } + + + /** Adds an interceptor definition to the definition cache. */ + public any function intercept(string beanName, string interceptorName, string methods = "") + { + var interceptDefinition = + { + name = arguments.interceptorName, + methods = arguments.methods + }; + + + arguments.beanName = trim(arguments.beanName); + + + // Determine if this is a name match or regex match. + if (len(arguments.beanName) && left(arguments.beanName, 1) == "/" && right(arguments.beanName, 1) == "/") + { + // Store regex without the forward slashes. + interceptDefinition.regex = mid(arguments.beanName, 2, len(arguments.beanName) - 2); + + arrayAppend(variables.interceptorCache.regex, interceptDefinition); + } + else + { + if (!structKeyExists(variables.interceptorCache.name, arguments.beanName)) + { + variables.interceptorCache.name[arguments.beanName] = []; + } + + arrayAppend(variables.interceptorCache.name[arguments.beanName], interceptDefinition); + } + + + return this; + } + + + /** Adds an interceptor definition to the definition cache. */ + public any function interceptByType(string type, string interceptorName, string methods = "") + { + var interceptDefinition = + { + type = arguments.type, + name = arguments.interceptorName, + methods = arguments.methods + }; + + arrayAppend(variables.interceptorCache.type, interceptDefinition); + } + + + + + // --------------- // + // PRIVATE METHODS // + // --------------- // + + /** Hook point to wrap bean with proxy. */ + private any function construct(string dottedPath) + { + var bean = super.construct(arguments.dottedPath); + var beanProxy = ""; + + // if it doesn't have a dotted path for us to create a new instance + // or it has no interceptors, we have to leave it alone + if (!hasInterceptors(arguments.dottedPath)) + { + return bean; + } + + // Create and return a proxy wrapping the bean. + beanProxy = new framework.beanProxy(bean, getInterceptorsForBean(arguments.dottedPath), variables.config); + + return beanProxy; + } + + + /** Gets the associated interceptor definitions for a specific bean. */ + private array function getInterceptorsForBean(string dottedPath) + { + // build the interceptor array: + var beanName = listLast(arguments.dottedPath, "."); + var beanNames = getAliases(beanName); + var beanTypes = ""; + var interceptDefinition = ""; + var interceptedBeanName = ""; + var interceptors = []; + + + arrayPrepend(beanNames, beanName); + + + // Grab all name based interceptors that match. + for (interceptedBeanName in beanNames) + { + // Match on name. + if (structKeyExists(variables.interceptorCache.name, interceptedBeanName)) + { + for (interceptDefinition in variables.interceptorCache.name[interceptedBeanName]) + { + arrayAppend(interceptors, {bean = getBean(interceptDefinition.name), methods = interceptDefinition.methods}); + } + } + } + + + // Match on regex. Ensure we only attach each one time. + if (arrayLen(variables.interceptorCache.regex)) + { + for (interceptDefinition in variables.interceptorCache.regex) + { + for (interceptedBeanName in beanNames) + { + if (reFindNoCase(interceptDefinition.regex, interceptedBeanName)) + { + arrayAppend(interceptors, {bean = getBean(interceptDefinition.name), methods = interceptDefinition.methods}); + break; + } + } + } + } + + + // Grab all type based interceptors that match. + if (arrayLen(variables.interceptorCache.type)) + { + beanTypes = getBeanTypes(arguments.dottedPath); + + for (interceptDefinition in variables.interceptorCache.type) + { + if (listFindNoCase(beanTypes, interceptDefinition.type)) + { + arrayAppend(interceptors, {bean = getBean(interceptDefinition.name), methods = interceptDefinition.methods}); + } + } + } + + + return interceptors; + } + + + /** Determines if the bean has interceptor definitions associated with it. */ + private boolean function hasInterceptors(string dottedPath) + { + var interceptedBeanName = ""; + var interceptorDefinition = {}; + var beanName = listLast(arguments.dottedPath, "."); + var beanNames = getAliases(beanName); + var beanTypes = ""; + + + arrayPrepend(beanNames, beanName); + + + for (interceptedBeanName in beanNames) + { + // Look for matches on name first. + if (structKeyExists(variables.interceptorCache.name, interceptedBeanName)) + { + return true; + } + + + // Look for matches on regex. + if (arrayLen(variables.interceptorCache.regex)) + { + for (interceptorDefinition in variables.interceptorCache.regex) + { + if (reFindNoCase(interceptorDefinition.regex, interceptedBeanName)) + { + return true; + } + } + } + + + // Look for matches by bean type. + if (arrayLen(variables.interceptorCache.type)) + { + beanTypes = getBeanTypes(arguments.dottedPath); + + for (interceptorDefinition in variables.interceptorCache.type) + { + if (listFindNoCase(beanTypes, interceptorDefinition.type)) + { + return true; + } + } + } + } + + + return false; + } + + + /** Finds all aliases for the given beanName. */ + private array function getAliases(string beanName) + { + var aliases = []; + var beanData = ""; + var key = ""; + + + if (structKeyExists(variables.beanInfo, arguments.beanName)) + { + beanData = variables.beanInfo[arguments.beanName]; + + for (key in variables.beanInfo) + { + // Same cfc dotted path, must be an alias. + if ( + key != arguments.beanName && + structKeyExists(variables.beanInfo[key], "cfc") && + structKeyExists(variables.beanInfo[arguments.beanName], "cfc") && + variables.beanInfo[key].cfc == variables.beanInfo[arguments.beanName].cfc) + { + arrayAppend(aliases, key); + } + } + } + + return aliases; + } + + + /** Returns a list of bean types (both name and dotted path) for a given bean. */ + private string function getBeanTypes(string dottedPath) + { + var beanTypes = ""; + var metadata = getComponentMetadata(arguments.dottedPath); + + while (!len(beanTypes) || structKeyExists(metadata, "extends")) + { + beanTypes = listAppend(beanTypes, listLast(metadata.name, ".")); + beanTypes = listAppend(beanTypes, metadata.name); + + if (structKeyExists(metadata, "extends")) + { + metadata = metadata.extends; + } + } + + return beanTypes; + } + + + /** Loads an array of interceptor definitions into the interceptor definition cache. */ + private void function loadInterceptors(array interceptors) + { + var interceptor = false; + + for (interceptor in interceptors) + { + if (structKeyExists(interceptor, "beanName")) + { + intercept(argumentCollection = interceptor); + } + else + { + interceptByType(argumentCollection = interceptor); + } + } + } + + + private void function setupFrameworkDefaults() + { + super.setupFrameworkDefaults(); + variables.config.version = variables._aop1_version & " (" & variables._di1_version & ")"; + } +} diff --git a/framework/beanProxy.cfc b/framework/beanProxy.cfc old mode 100755 new mode 100644 index db6e1056..c799fd5b --- a/framework/beanProxy.cfc +++ b/framework/beanProxy.cfc @@ -1,936 +1,936 @@ -component { - variables._fw1_version = "3.5.1"; - variables._aop1_version = "2.0.1"; -/* - Copyright (c) 2013-2015, Mark Drew, Sean Corfield, Daniel Budde - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - - - - variables.afterInterceptors = []; - variables.aroundInterceptors = []; - variables.beforeInterceptors = []; - variables.errorInterceptors = []; - variables.interceptedMethods = ""; - variables.interceptID = createUUID(); - variables.preName = "___"; - variables.targetBean = ""; - variables.targetBeanPath = ""; - - - - - // -------------- // - // PUBLIC METHODS // - // -------------- // - - /** Constructor. */ - public any function init(required any bean, required array interceptors, required struct config) - { - variables.targetBean = arguments.bean; - - populateInterceptorCache(arguments.interceptors); - morphTargetBean(arguments.config); - morphProxy(arguments.config); - cleanVarScope(); - - return this; - } - - - /** Entry point for all publically accessible intercepted methods. */ - public any function onMissingMethod(string missingMethodName, struct missingMethodArguments = {}) - { - // Prevent infinite loop and make sure the method is publically accessible. - if (!structKeyExists(variables.targetBean, arguments.missingMethodName) && !structKeyExists(variables.targetBean, variables.preName & arguments.missingMethodName)) - { - objectName = listLast(getMetadata(this).name, "."); - throw( message="Unable to locate method in (" & objectName & ").", - detail="The method (" & arguments.missingMethodName & ") could not be found. Please verify the method exists and is publically accessible."); - } - - - local.result = runStacks(arguments.missingMethodName, arguments.missingMethodArguments); - - if (structKeyExists(local, "result") && !isNull(local.result)) return local.result; - } - - - /** Runs all the interceptor stacks. */ - public any function runStacks(string methodName, struct args = {}) - { - var objectName = ""; - - - // Prevent infinite loop and make sure the method exists (public or private) - if (!variables.targetBean.$methodExists(arguments.methodName) && !variables.targetBean.$methodExists(variables.preName & arguments.methodName)) - { - objectName = listLast(getMetadata(this).name, "."); - throw(message="Unable to locate method in (" & objectName & ").", detail="The method (" & arguments.methodName & ") could not be found."); - } - - - try - { - // Intercepted method call - if (variables.interceptedMethods == "*" || listFindNoCase(variables.interceptedMethods, arguments.methodName)) - { - runBeforeStack(arguments.methodName, arguments.args); - local.result = runAroundStack(arguments.methodName, arguments.args); - local.result = runAfterStack(arguments.methodName, arguments.args, !structKeyExists(local, "result") || isNull(local.result) ? javacast("null", 0) : local.result); - } - - // Non-intercepted method call - else - { - local.result = variables.targetBean.$call(arguments.methodName, arguments.args); - } - } - catch (any exception) - { - if (arrayLen(variables.errorInterceptors)) - { - runOnErrorStack(arguments.methodName, arguments.args, exception); - } - else - { - rethrow; - } - } - - - if (structKeyExists(local, "result") && !isNull(local.result)) return local.result; - } - - - - - // --------------- // - // PRIVATE METHODS // - // --------------- // - - // --- Interceptor Augmentation Methods --- // - - /** Used to setup intercepted method lists on a per bean basis. */ - public any function _addInterceptedMethods(required string interceptID, required string methods) - { - var interceptedMethods = ""; - var methodName = ""; - - - if (!structKeyExists(variables, "interceptedMethods")) - { - variables.interceptedMethods = {}; - } - - if (!structKeyExists(variables.interceptedMethods, arguments.interceptID)) - { - variables.interceptedMethods[arguments.interceptID] = ""; - } - - interceptedMethods = variables.interceptedMethods[arguments.interceptID]; - - - if (interceptedMethods != "*") - { - if (arguments.methods == "" || arguments.methods == "*") - { - variables.interceptedMethods[arguments.interceptID] = "*"; - } - else - { - for (methodName in listToArray(arguments.methods)) - { - if (!listFindNoCase(variables.interceptedMethods[arguments.interceptID], methodName)) - { - interceptedMethods = listAppend(interceptedMethods, methodName); - } - } - - - interceptedMethods = listSort(interceptedMethods, "textnocase"); - variables.interceptedMethods[arguments.interceptID] = interceptedMethods; - } - } - } - - - /** Used to setup intercepted method lists on a per bean basis. */ - public any function _getInterceptedMethods(string interceptID) - { - var methods = {}; - - if (structKeyExists(variables, "interceptedMethods")) - { - methods = variables.interceptedMethods; - } - - if (!structKeyExists(arguments, "interceptID")) - { - return methods; - } - - - if (structKeyExists(methods, arguments.interceptID)) - { - return methods[arguments.interceptID]; - } - - return ""; - } - - - /** Used to inject methods and data. */ - public any function _inject(required string key, required any value, required string access="public") - { - if (arguments.access == "public") - { - this[arguments.key] = arguments.value; - } - - variables[arguments.key] = arguments.value; - } - - - /** Determines if an around interceptor is the last in the call chain. */ - public boolean function _isLast() - { - return isSimpleValue(variables.nextInterceptor); - } - - - /** Runs the 'Around' method, skips to the next interceptor in the chain if the 'Around' should not be run, or calls the actual method. */ - public any function _preAround(required any targetBean, required string methodName, struct args = {}) - { - var interceptedMethods = getInterceptedMethods(arguments.targetBean.interceptID); - - // Match if method is to be intercepted by this interceptor. - if (interceptedMethods == "*" || listFindNoCase(interceptedMethods, arguments.methodName)) - { - local.result = around(arguments.targetBean, arguments.methodName, arguments.args); - } - else - { - local.result = proceed(arguments.targetBean, arguments.methodName, arguments.args); - } - - if (structKeyExists(local, "result") && !isNull(local.result)) return local.result; - } - - - /** Runs the next around interceptor or processes the method if it is the final interceptor in the call chain. */ - public any function _proceed(required any targetBean, required string methodName, struct args = {}) - { - if (isLast()) - { - local.result = arguments.targetBean.$call(arguments.methodName, arguments.args, true); - } - else - { - local.result = variables.nextInterceptor.preAround(arguments.targetBean, arguments.methodName, arguments.args); - } - - - if (structKeyExists(local, "result") && !isNull(local.result)) return local.result; - } - - - /** Helper method for use inside of (after, around, before) to translate position based 'args' into name based. */ - private any function _translateArgs(required any targetBean, required string methodName, required struct args, boolean replace = false) - { - var i = 1; - var key = ""; - var argumentInfo = arguments.targetBean.$getArgumentInfo(arguments.methodName); - var resultArgs = {}; - - if (structIsEmpty(arguments.args) || !structKeyExists(arguments.args, "1")) - { - return arguments.args; - } - - for (i = 1; i <= arrayLen(argumentInfo); i++) - { - resultArgs[argumentInfo[i].name] = arguments.args[i]; - } - - if (arguments.replace) - { - structAppend(arguments.args, resultArgs, true); - - for (key in arguments.args) - { - if (isNumeric(key)) - { - structDelete(arguments.args, key); - } - } - } - - return resultArgs; - } - - - - - // --- Target Bean and Proxy Bean Augmentation Methods --- // - - /** Runs the appropriate method on the target bean. */ - private any function $call(required string methodName, struct args = {}, boolean original = false) - { - if (arguments.original) - { - local.result = evaluate(variables.preName & arguments.methodName & "(argumentCollection = arguments.args)"); - } - else - { - local.result = evaluate(arguments.methodName & "(argumentCollection = arguments.args)"); - } - - if (structKeyExists(local, "result") && !isNull(local.result)) return local.result; - } - - - /** Used to replace any 'private' methods on the target bean that are being intercepted. Creates an intercept point. */ - public any function $callPrivateMethod() - { - local.methodName = getFunctionCalledName(); - local.result = $callStacks(local.methodName, arguments); - - if (structKeyExists(local, "result") && !isNull(local.result)) return local.result; - } - - - /** Used to replace any 'public' methods on the target bean that are being intercepted. Creates an intercept point. */ - public any function $callPublicMethod() - { - local.methodName = getFunctionCalledName(); - local.result = $callStacks(local.methodName, arguments); - - if (structKeyExists(local, "result") && !isNull(local.result)) return local.result; - } - - - /** Method called by the intercept points to start the stack run if needed. */ - private any function $callStacks(string methodName, struct args = {}) - { - local.result = variables.beanProxy.runStacks(arguments.methodName, arguments.args); - - if (structKeyExists(local, "result") && !isNull(local.result)) return local.result; - } - - - /** Gets arguments information for a method. */ - private array function $getArgumentInfo(required string methodName) - { - var method = ""; - var methodMetadata = ""; - - - if (structKeyExists(this, variables.preName & arguments.methodName)) - { - method = this[variables.preName & arguments.methodName]; - } - else if (structKeyExists(this, arguments.methodName)) - { - method = this[arguments.methodName]; - } - else if (structKeyExists(variables, variables.preName & arguments.methodName)) - { - method = variables[variables.preName & arguments.methodName]; - } - else if (structKeyExists(variables, arguments.methodName)) - { - method = variables[arguments.methodName]; - } - - - if (!isSimpleValue(method)) - { - methodMetadata = getMetadata(method); - - if (structKeyExists(methodMetadata, "parameters") && arrayLen(methodMetadata.parameters)) - { - return methodMetadata.parameters; - } - } - - - return []; - } - - - /** Runs the appropriate method on the target bean. */ - private boolean function $methodExists(required string methodName) - { - return structKeyExists(this, arguments.methodName) || structKeyExists(variables, arguments.methodName); - } - - - /** A pass through method placed on the proxy bean (used primarily for 'init', 'set..', and 'initMethod' on target bean). */ - private any function $passThrough() - { - local.methodName = getFunctionCalledName(); - local.result = evaluate("variables.targetBean." & local.methodName & "(argumentCollection = arguments)"); - - if (structKeyExists(local, "result") && !isNull(local.result)) return local.result; - } - - - /** Used to inject methods on the target bean. */ - public any function $replaceMethod(required string methodName, required any implementedMethod, required string access="public") - { - var method = ""; - - if (arguments.access == "public") - { - method = this[arguments.methodName]; - - if (isCustomFunction(method)) - { - this[variables.preName & arguments.methodName] = this[arguments.methodName]; - this[arguments.methodName] = arguments.implementedMethod; - } - } - - method = variables[arguments.methodName]; - - if (isCustomFunction(method)) - { - variables[variables.preName & arguments.methodName] = variables[arguments.methodName]; - variables[arguments.methodName] = arguments.implementedMethod; - } - } - - - - - // --- Local Private Methods --- // - - /** Adds an interceptor definition and bean to the interceptor cache for the proxied bean. */ - private void function addInterceptor(required any interceptor) - { - // If someone decides to have an interceptor handle multiple interceptor types, go for it. - - if (hasAfterMethod(arguments.interceptor)) - { - arrayAppend(variables.afterInterceptors, arguments.interceptor); - } - - - if (hasAroundMethod(arguments.interceptor)) - { - arrayAppend(variables.aroundInterceptors, arguments.interceptor); - } - - - if (hasBeforeMethod(arguments.interceptor)) - { - arrayAppend(variables.beforeInterceptors, arguments.interceptor); - } - - - if (hasOnErrorMethod(arguments.interceptor)) - { - arrayAppend(variables.errorInterceptors, arguments.interceptor); - } - - - if (!structKeyExists(arguments.interceptor.bean, "interceptorAugmented")) - { - augmentInterceptor(arguments.interceptor); - } - - - // Maintain the list of intercepted methods. '*' and blank means all. - if (variables.interceptedMethods != "*") - { - if (!len(arguments.interceptor.methods) || arguments.interceptor.methods == "*") - { - variables.interceptedMethods = "*"; - } - else - { - variables.interceptedMethods = listSort(listAppend(variables.interceptedMethods, arguments.interceptor.methods), "textnocase"); - } - } - - - // Update the interceptor itself. - arguments.interceptor.bean.addInterceptedMethods(variables.interceptID, arguments.interceptor.methods); - } - - - /** Adds variables and methods needed by Around interceptors. */ - private void function augmentAroundInterceptor(required any interceptor) - { - var prevInterceptor = ""; - - - // Add additional methods for an around interceptor. - arguments.interceptor.bean._inject("isLast", _isLast); - arguments.interceptor.bean._inject("preAround", _preAround); - arguments.interceptor.bean._inject("proceed", _proceed); - arguments.interceptor.bean._inject("nextInterceptor", "", "private"); - - - // Add a link in the call chain from the previous interceptor to the one just added. - if (1 < arrayLen(variables.aroundInterceptors)) - { - prevInterceptor = variables.aroundInterceptors[arrayLen(variables.aroundInterceptors) - 1]; - - prevInterceptor.bean._inject = _inject; - - prevInterceptor.bean._inject("nextInterceptor", arguments.interceptor.bean, "private"); - - structDelete(prevInterceptor.bean, "_inject"); - } - } - - - /** Adds variables and methods needed by all interceptors. */ - private void function augmentInterceptor(required any interceptor) - { - var interceptorVarScope = ""; - - - if (!structKeyExists(arguments.interceptor, "methods") || !len(arguments.interceptor.methods)) - { - arguments.interceptor.methods = "*"; - } - - arguments.interceptor.bean._inject = _inject; - - arguments.interceptor.bean._inject("interceptorAugmented", true); - arguments.interceptor.bean._inject("addInterceptedMethods", _addInterceptedMethods); - arguments.interceptor.bean._inject("getInterceptedMethods", _getInterceptedMethods); - arguments.interceptor.bean._inject("translateArgs", _translateArgs, "private"); - - if (hasAroundMethod(arguments.interceptor)) - { - augmentAroundInterceptor(arguments.interceptor); - } - - structDelete(arguments.interceptor.bean, "_inject"); - } - - - /** Cleans up temporary methods from the variables scope. */ - private void function cleanVarScope() - { - var key = ""; - - for (key in variables) - { - if (left(key, 1) == "_" || left(key, 1) == "$") - { - structDelete(variables, key); - } - } - } - - - /** Returns whether a method's access is public or private. */ - private string function getMethodAccess(any method) - { - var access = "public"; - var methodMetadata = getMetadata(method); - - if (structKeyExists(methodMetadata, "access") && methodMetadata.access == "private") - { - access = "private"; - } - - return access; - } - - - /** Retrieves property and method info about the targetBean. */ - private struct function getTargetBeanMetadata(any beanMetadata) - { - var beanInfo = {accessors = false, methods = {}, name = "", properties = {}}; - var i = 0; - var method = {}; - var property = {}; - var tmpBeanInfo = {}; - - - if (isObject(arguments.beanMetadata)) - { - arguments.beanMetadata = getMetadata(arguments.beanMetadata); - } - - - if (structKeyExists(arguments.beanMetadata, "accessors")) - { - beanInfo.accessors = arguments.beanMetadata.accessors; - } - - - if (structKeyExists(arguments.beanMetadata, "name")) - { - beanInfo.name = arguments.beanMetadata.name; - } - - - // Gather method information. - if (structKeyExists(arguments.beanMetadata, "functions")) - { - // ACF 9 did NOT like using a for-in loop here. - for (i = 1; i <= arrayLen(arguments.beanMetadata.functions); i++) - { - method = arguments.beanMetadata.functions[i]; - beanInfo.methods[method.name] = {}; - - if (structKeyExists(method, "access")) - { - beanInfo.methods[method.name]["access"] = method.access; - } - } - } - - - // Gather property information. - if (structKeyExists(arguments.beanMetadata, "properties")) - { - // ACF 9 did NOT like using a for-in loop here. - for (i = 1; i <= arrayLen(arguments.beanMetadata.properties); i++) - { - property = arguments.beanMetadata.properties[i]; - beanInfo.properties[property.name] = {}; - - if (structKeyExists(property, "access")) - { - beanInfo.properties[property.name]["access"] = property.access; - } - } - } - - - // Handle 'extends' hierarchy info. - if (structKeyExists(arguments.beanMetadata, "extends")) - { - tmpBeanInfo = getTargetBeanMetadata(arguments.beanMetadata.extends); - structAppend(beanInfo.properties, tmpBeanInfo.properties); - structAppend(beanInfo.methods, tmpBeanInfo.methods); - } - - - return beanInfo; - } - - - /** Gathers all the method information for the targetBean. */ - private struct function getTargetBeanMethodInfo() - { - var beanInfo = getTargetBeanMetadata(variables.targetBean); - var key = ""; - var methodInfo = {}; - - - variables.targetBeanPath = beanInfo.name; - - - // Locate methods in 'this' scope of targetBean. - for (key in variables.targetBean) - { - if (!structKeyExists(methodInfo, key) && isCustomFunction(variables.targetBean[key])) - { - methodInfo[key] = {access = "public", discoveredIn = "this", isPropertyAccessor = false}; - } - } - - - // Locate any missing 'set' and 'get' methods only present in the metadata. - if (beanInfo.accessors) - { - for (key in beanInfo.methods) - { - if (!structKeyExists(methodInfo, key)) - { - methodInfo[key] = {access = beanInfo.methods[key].access, discoveredIn = "metadata", isPropertyAccessor = false}; - } - } - } - - - // Determine if any of the 'set' or 'get' methods match a property. - if (beanInfo.accessors) - { - for (key in beanInfo.properties) - { - if (structKeyExists(methodInfo, "set" & key)) - { - methodInfo["set" & key].isPropertyAccessor = true; - } - - - if (structKeyExists(methodInfo, "get" & key)) - { - methodInfo["get" & key].isPropertyAccessor = true; - } - } - } - - - return methodInfo; - } - - - /** Determines if an interceptor has an After method. */ - private boolean function hasAfterMethod(required any interceptor) - { - return structKeyExists(arguments.interceptor.bean, "after"); - } - - - /** Determines if an interceptor has an Around method. */ - private boolean function hasAroundMethod(required any interceptor) - { - return structKeyExists(arguments.interceptor.bean, "around"); - } - - - /** Determines if an interceptor has a Before method. */ - private boolean function hasBeforeMethod(required any interceptor) - { - return structKeyExists(arguments.interceptor.bean, "before"); - } - - - /** Determines if an interceptor has an onError method. */ - private boolean function hasOnErrorMethod(required any interceptor) - { - return structKeyExists(arguments.interceptor.bean, "onError"); - } - - - /** Determines if a 'methodName' is in a list of methods. A blank list of method matches will be an automatic match. */ - private boolean function methodMatches(string methodName, string matchers) - { - // Match on: 1) No matches provided 2) Method name in matchers - return !listLen(arguments.matchers) || arguments.matchers == "*" || listFindNoCase(arguments.matchers, arguments.methodName); - } - - - /** Alters the proxy bean so the factory still sees the set..(), init(), and initMethod() and so these methods get called on the target bean. */ - private void function morphProxy(required struct config) - { - var key = ""; - - // Handle the 'set...' methods. - for (key in variables.targetBean) - { - if (left(key, 3) == "set") - { - this[key] = $passThrough; - } - } - - - // Checks to see if the 'initMethod' was defined in the config and handles if it exists on the target bean. - if (structKeyExists(arguments.config, "initMethod") && len(arguments.config.initMethod) && structKeyExists(variables.targetBean, arguments.config.initMethod)) - { - this[arguments.config.initMethod] = $passThrough; - } - - - // Passes the init() if it exists, otherwise removes it. - if (structKeyExists(variables.targetBean, "init")) - { - this["init"] = $passThrough; - } - else - { - structDelete(this, "init"); - } - } - - - /** Alters the target bean by adding intercept points. */ - private void function morphTargetBean(required struct config) - { - var access = ""; - var beanMethodInfo = getTargetBeanMethodInfo(); - var initMethod = ""; - var key = ""; - var method = ""; - var methodInfo = ""; - - - if (structKeyExists(arguments.config, "initMethod")) - { - initMethod = arguments.config.initMethod; - } - - - variables.targetBean.$inject = _inject; - variables.targetBean.$replaceMethod = $replaceMethod; - - - // Setup internal variables and methods on the target bean. - variables.targetBean.$inject("beanProxy", this, "private"); - variables.targetBean.$inject("preName", variables.preName, "private"); - variables.targetBean.$inject("$callStacks", $callStacks, "private"); - variables.targetBean.$inject("$call", $call); - variables.targetBean.$inject("$methodExists", $methodExists); - variables.targetBean.$inject("$getArgumentInfo", $getArgumentInfo); - variables.targetBean.$inject("interceptID", variables.interceptID); - - - for (key in beanMethodInfo) - { - methodInfo = beanMethodInfo[key]; - - - // Only alter methods that should be intercepted. 'init()', accessors, and 'initMethod' are ignored unless specified in the methods list. - if ( - (variables.interceptedMethods == "*" && key != "init" && key != initMethod && !methodInfo.isPropertyAccessor) || - listFindNoCase(variables.interceptedMethods, key) - ) - { - // Handle methods listed in a scope. - if (listFindNoCase("this,variables", methodInfo.discoveredIn)) - { - // Handle methods found in 'this' scope. - if (methodInfo.access == "public") - { - variables.targetBean.$replaceMethod(key, $callPublicMethod); - } - - // Handle methods in the variables scope. - else - { - variables.targetBean.$replaceMethod(key, $callPublicMethod, "private"); - } - } - - - // Handle methods found only in the metadata. - else - { - try - { - if (methodInfo.access == "public") - { - variables.targetBean.$replaceMethod(key, $callPublicMethod); - } - else - { - variables.targetBean.$replaceMethod(key, $callPublicMethod, "private"); - } - } - catch (any exception) - { - throw(message="Unable to locate the method (" & key & ") on target bean (" & variables.targetBeanPath & ")."); - } - } - } - } - - - structDelete(variables.targetBean, "$inject"); - structDelete(variables.targetBean, "$replaceMethod"); - } - - - /** Adds an array of interceptor definitions to the interceptor definition cache. */ - private void function populateInterceptorCache(required array interceptors) - { - var interceptor = ""; - - for (interceptor in arguments.interceptors) - { - addInterceptor(interceptor); - } - } - - - private any function runAfterStack(string methodName, struct args, any result) - { - if (structKeyExists(arguments, "result") && !isNull(arguments.result)) - { - local.result = arguments.result; - } - - - for (local.interceptor in variables.afterInterceptors) - { - if (methodMatches(methodName, local.interceptor.methods)) - { - local.tempResult = local.interceptor.bean.after(variables.targetBean, arguments.methodName, args, isNull(arguments.result) ? javacast("null", 0) : arguments.result); - } - - if (structKeyExists(local, "tempResult")) - { - if (!isNull(local.tempResult)) - { - local.result = local.tempResult; - } - - structDelete(local, "tempResult"); - } - } - - - if (structKeyExists(local, "result") && !isNull(local.result)) return local.result; - } - - - private any function runAroundStack(string methodName, struct args) - { - if (arrayLen(variables.aroundInterceptors)) - { - // Only need to call the first one in the chain to start the process. - local.result = variables.aroundInterceptors[1].bean.preAround(variables.targetBean, arguments.methodName, arguments.args); - } - else - { - local.result = variables.targetBean.$call(arguments.methodName, arguments.args, true); - } - - if (structKeyExists(local, "result") and !isNull(local.result)) return local.result; - } - - - private void function runBeforeStack(string methodName, struct args) - { - var inteceptor = ""; - - for (inteceptor in variables.beforeInterceptors) - { - if (structKeyExists(inteceptor.bean, "before")) - { - if (methodMatches(arguments.methodName, inteceptor.methods)) - { - inteceptor.bean.before(variables.targetBean, arguments.methodName, arguments.args); - } - } - } - } - - - private void function runOnErrorStack(string methodName, struct args, any exception) - { - var interceptor = ""; - - for (interceptor in variables.errorInterceptors) - { - if (methodMatches(arguments.methodName, interceptor.methods)) - { - interceptor.bean.onError(variables.targetBean, arguments.methodName, arguments.args, arguments.exception); - } - } - } -} +component { + variables._fw1_version = "4.0.0"; + variables._aop1_version = "2.0.2"; +/* + Copyright (c) 2013-2016, Mark Drew, Sean Corfield, Daniel Budde + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + + + + variables.afterInterceptors = []; + variables.aroundInterceptors = []; + variables.beforeInterceptors = []; + variables.errorInterceptors = []; + variables.interceptedMethods = ""; + variables.interceptID = createUUID(); + variables.preName = "___"; + variables.targetBean = ""; + variables.targetBeanPath = ""; + + + + + // -------------- // + // PUBLIC METHODS // + // -------------- // + + /** Constructor. */ + public any function init(required any bean, required array interceptors, required struct config) + { + variables.targetBean = arguments.bean; + + populateInterceptorCache(arguments.interceptors); + morphTargetBean(arguments.config); + morphProxy(arguments.config); + cleanVarScope(); + + return this; + } + + + /** Entry point for all publically accessible intercepted methods. */ + public any function onMissingMethod(string missingMethodName, struct missingMethodArguments = {}) + { + // Prevent infinite loop and make sure the method is publically accessible. + if (!structKeyExists(variables.targetBean, arguments.missingMethodName) && !structKeyExists(variables.targetBean, variables.preName & arguments.missingMethodName)) + { + objectName = listLast(getMetadata(this).name, "."); + throw( message="Unable to locate method in (" & objectName & ").", + detail="The method (" & arguments.missingMethodName & ") could not be found. Please verify the method exists and is publically accessible."); + } + + + local.result = runStacks(arguments.missingMethodName, arguments.missingMethodArguments); + + if (structKeyExists(local, "result") && !isNull(local.result)) return local.result; + } + + + /** Runs all the interceptor stacks. */ + public any function runStacks(string methodName, struct args = {}) + { + var objectName = ""; + + + // Prevent infinite loop and make sure the method exists (public or private) + if (!variables.targetBean.$methodExists(arguments.methodName) && !variables.targetBean.$methodExists(variables.preName & arguments.methodName)) + { + objectName = listLast(getMetadata(this).name, "."); + throw(message="Unable to locate method in (" & objectName & ").", detail="The method (" & arguments.methodName & ") could not be found."); + } + + + try + { + // Intercepted method call + if (variables.interceptedMethods == "*" || listFindNoCase(variables.interceptedMethods, arguments.methodName)) + { + runBeforeStack(arguments.methodName, arguments.args); + local.result = runAroundStack(arguments.methodName, arguments.args); + local.result = runAfterStack(arguments.methodName, arguments.args, !structKeyExists(local, "result") || isNull(local.result) ? javacast("null", 0) : local.result); + } + + // Non-intercepted method call + else + { + local.result = variables.targetBean.$call(arguments.methodName, arguments.args); + } + } + catch (any exception) + { + if (arrayLen(variables.errorInterceptors)) + { + runOnErrorStack(arguments.methodName, arguments.args, exception); + } + else + { + rethrow; + } + } + + + if (structKeyExists(local, "result") && !isNull(local.result)) return local.result; + } + + + + + // --------------- // + // PRIVATE METHODS // + // --------------- // + + // --- Interceptor Augmentation Methods --- // + + /** Used to setup intercepted method lists on a per bean basis. */ + public any function _addInterceptedMethods(required string interceptID, required string methods) + { + var interceptedMethods = ""; + var methodName = ""; + + + if (!structKeyExists(variables, "interceptedMethods")) + { + variables.interceptedMethods = {}; + } + + if (!structKeyExists(variables.interceptedMethods, arguments.interceptID)) + { + variables.interceptedMethods[arguments.interceptID] = ""; + } + + interceptedMethods = variables.interceptedMethods[arguments.interceptID]; + + + if (interceptedMethods != "*") + { + if (arguments.methods == "" || arguments.methods == "*") + { + variables.interceptedMethods[arguments.interceptID] = "*"; + } + else + { + for (methodName in listToArray(arguments.methods)) + { + if (!listFindNoCase(variables.interceptedMethods[arguments.interceptID], methodName)) + { + interceptedMethods = listAppend(interceptedMethods, methodName); + } + } + + + interceptedMethods = listSort(interceptedMethods, "textnocase"); + variables.interceptedMethods[arguments.interceptID] = interceptedMethods; + } + } + } + + + /** Used to setup intercepted method lists on a per bean basis. */ + public any function _getInterceptedMethods(string interceptID) + { + var methods = {}; + + if (structKeyExists(variables, "interceptedMethods")) + { + methods = variables.interceptedMethods; + } + + if (!structKeyExists(arguments, "interceptID")) + { + return methods; + } + + + if (structKeyExists(methods, arguments.interceptID)) + { + return methods[arguments.interceptID]; + } + + return ""; + } + + + /** Used to inject methods and data. */ + public any function _inject(required string key, required any value, required string access="public") + { + if (arguments.access == "public") + { + this[arguments.key] = arguments.value; + } + + variables[arguments.key] = arguments.value; + } + + + /** Determines if an around interceptor is the last in the call chain. */ + public boolean function _isLast() + { + return isSimpleValue(variables.nextInterceptor); + } + + + /** Runs the 'Around' method, skips to the next interceptor in the chain if the 'Around' should not be run, or calls the actual method. */ + public any function _preAround(required any targetBean, required string methodName, struct args = {}) + { + var interceptedMethods = getInterceptedMethods(arguments.targetBean.interceptID); + + // Match if method is to be intercepted by this interceptor. + if (interceptedMethods == "*" || listFindNoCase(interceptedMethods, arguments.methodName)) + { + local.result = around(arguments.targetBean, arguments.methodName, arguments.args); + } + else + { + local.result = proceed(arguments.targetBean, arguments.methodName, arguments.args); + } + + if (structKeyExists(local, "result") && !isNull(local.result)) return local.result; + } + + + /** Runs the next around interceptor or processes the method if it is the final interceptor in the call chain. */ + public any function _proceed(required any targetBean, required string methodName, struct args = {}) + { + if (isLast()) + { + local.result = arguments.targetBean.$call(arguments.methodName, arguments.args, true); + } + else + { + local.result = variables.nextInterceptor.preAround(arguments.targetBean, arguments.methodName, arguments.args); + } + + + if (structKeyExists(local, "result") && !isNull(local.result)) return local.result; + } + + + /** Helper method for use inside of (after, around, before) to translate position based 'args' into name based. */ + private any function _translateArgs(required any targetBean, required string methodName, required struct args, boolean replace = false) + { + var i = 1; + var key = ""; + var argumentInfo = arguments.targetBean.$getArgumentInfo(arguments.methodName); + var resultArgs = {}; + + if (structIsEmpty(arguments.args) || !structKeyExists(arguments.args, "1")) + { + return arguments.args; + } + + for (i = 1; i <= arrayLen(argumentInfo); i++) + { + resultArgs[argumentInfo[i].name] = arguments.args[i]; + } + + if (arguments.replace) + { + structAppend(arguments.args, resultArgs, true); + + for (key in arguments.args) + { + if (isNumeric(key)) + { + structDelete(arguments.args, key); + } + } + } + + return resultArgs; + } + + + + + // --- Target Bean and Proxy Bean Augmentation Methods --- // + + /** Runs the appropriate method on the target bean. */ + private any function $call(required string methodName, struct args = {}, boolean original = false) + { + if (arguments.original) + { + local.result = evaluate(variables.preName & arguments.methodName & "(argumentCollection = arguments.args)"); + } + else + { + local.result = evaluate(arguments.methodName & "(argumentCollection = arguments.args)"); + } + + if (structKeyExists(local, "result") && !isNull(local.result)) return local.result; + } + + + /** Used to replace any 'private' methods on the target bean that are being intercepted. Creates an intercept point. */ + public any function $callPrivateMethod() + { + local.methodName = getFunctionCalledName(); + local.result = $callStacks(local.methodName, arguments); + + if (structKeyExists(local, "result") && !isNull(local.result)) return local.result; + } + + + /** Used to replace any 'public' methods on the target bean that are being intercepted. Creates an intercept point. */ + public any function $callPublicMethod() + { + local.methodName = getFunctionCalledName(); + local.result = $callStacks(local.methodName, arguments); + + if (structKeyExists(local, "result") && !isNull(local.result)) return local.result; + } + + + /** Method called by the intercept points to start the stack run if needed. */ + private any function $callStacks(string methodName, struct args = {}) + { + local.result = variables.beanProxy.runStacks(arguments.methodName, arguments.args); + + if (structKeyExists(local, "result") && !isNull(local.result)) return local.result; + } + + + /** Gets arguments information for a method. */ + private array function $getArgumentInfo(required string methodName) + { + var method = ""; + var methodMetadata = ""; + + + if (structKeyExists(this, variables.preName & arguments.methodName)) + { + method = this[variables.preName & arguments.methodName]; + } + else if (structKeyExists(this, arguments.methodName)) + { + method = this[arguments.methodName]; + } + else if (structKeyExists(variables, variables.preName & arguments.methodName)) + { + method = variables[variables.preName & arguments.methodName]; + } + else if (structKeyExists(variables, arguments.methodName)) + { + method = variables[arguments.methodName]; + } + + + if (!isSimpleValue(method)) + { + methodMetadata = getMetadata(method); + + if (structKeyExists(methodMetadata, "parameters") && arrayLen(methodMetadata.parameters)) + { + return methodMetadata.parameters; + } + } + + + return []; + } + + + /** Runs the appropriate method on the target bean. */ + private boolean function $methodExists(required string methodName) + { + return structKeyExists(this, arguments.methodName) || structKeyExists(variables, arguments.methodName); + } + + + /** A pass through method placed on the proxy bean (used primarily for 'init', 'set..', and 'initMethod' on target bean). */ + private any function $passThrough() + { + local.methodName = getFunctionCalledName(); + local.result = evaluate("variables.targetBean." & local.methodName & "(argumentCollection = arguments)"); + + if (structKeyExists(local, "result") && !isNull(local.result)) return local.result; + } + + + /** Used to inject methods on the target bean. */ + public any function $replaceMethod(required string methodName, required any implementedMethod, required string access="public") + { + var method = ""; + + if (arguments.access == "public") + { + method = this[arguments.methodName]; + + if (isCustomFunction(method)) + { + this[variables.preName & arguments.methodName] = this[arguments.methodName]; + this[arguments.methodName] = arguments.implementedMethod; + } + } + + method = variables[arguments.methodName]; + + if (isCustomFunction(method)) + { + variables[variables.preName & arguments.methodName] = variables[arguments.methodName]; + variables[arguments.methodName] = arguments.implementedMethod; + } + } + + + + + // --- Local Private Methods --- // + + /** Adds an interceptor definition and bean to the interceptor cache for the proxied bean. */ + private void function addInterceptor(required any interceptor) + { + // If someone decides to have an interceptor handle multiple interceptor types, go for it. + + if (hasAfterMethod(arguments.interceptor)) + { + arrayAppend(variables.afterInterceptors, arguments.interceptor); + } + + + if (hasAroundMethod(arguments.interceptor)) + { + arrayAppend(variables.aroundInterceptors, arguments.interceptor); + } + + + if (hasBeforeMethod(arguments.interceptor)) + { + arrayAppend(variables.beforeInterceptors, arguments.interceptor); + } + + + if (hasOnErrorMethod(arguments.interceptor)) + { + arrayAppend(variables.errorInterceptors, arguments.interceptor); + } + + + if (!structKeyExists(arguments.interceptor.bean, "interceptorAugmented")) + { + augmentInterceptor(arguments.interceptor); + } + + + // Maintain the list of intercepted methods. '*' and blank means all. + if (variables.interceptedMethods != "*") + { + if (!len(arguments.interceptor.methods) || arguments.interceptor.methods == "*") + { + variables.interceptedMethods = "*"; + } + else + { + variables.interceptedMethods = listSort(listAppend(variables.interceptedMethods, arguments.interceptor.methods), "textnocase"); + } + } + + + // Update the interceptor itself. + arguments.interceptor.bean.addInterceptedMethods(variables.interceptID, arguments.interceptor.methods); + } + + + /** Adds variables and methods needed by Around interceptors. */ + private void function augmentAroundInterceptor(required any interceptor) + { + var prevInterceptor = ""; + + + // Add additional methods for an around interceptor. + arguments.interceptor.bean._inject("isLast", _isLast); + arguments.interceptor.bean._inject("preAround", _preAround); + arguments.interceptor.bean._inject("proceed", _proceed); + arguments.interceptor.bean._inject("nextInterceptor", "", "private"); + + + // Add a link in the call chain from the previous interceptor to the one just added. + if (1 < arrayLen(variables.aroundInterceptors)) + { + prevInterceptor = variables.aroundInterceptors[arrayLen(variables.aroundInterceptors) - 1]; + + prevInterceptor.bean._inject = _inject; + + prevInterceptor.bean._inject("nextInterceptor", arguments.interceptor.bean, "private"); + + structDelete(prevInterceptor.bean, "_inject"); + } + } + + + /** Adds variables and methods needed by all interceptors. */ + private void function augmentInterceptor(required any interceptor) + { + var interceptorVarScope = ""; + + + if (!structKeyExists(arguments.interceptor, "methods") || !len(arguments.interceptor.methods)) + { + arguments.interceptor.methods = "*"; + } + + arguments.interceptor.bean._inject = _inject; + + arguments.interceptor.bean._inject("interceptorAugmented", true); + arguments.interceptor.bean._inject("addInterceptedMethods", _addInterceptedMethods); + arguments.interceptor.bean._inject("getInterceptedMethods", _getInterceptedMethods); + arguments.interceptor.bean._inject("translateArgs", _translateArgs, "private"); + + if (hasAroundMethod(arguments.interceptor)) + { + augmentAroundInterceptor(arguments.interceptor); + } + + structDelete(arguments.interceptor.bean, "_inject"); + } + + + /** Cleans up temporary methods from the variables scope. */ + private void function cleanVarScope() + { + var key = ""; + + for (key in variables) + { + if (left(key, 1) == "_" || left(key, 1) == "$") + { + structDelete(variables, key); + } + } + } + + + /** Returns whether a method's access is public or private. */ + private string function getMethodAccess(any method) + { + var access = "public"; + var methodMetadata = getMetadata(method); + + if (structKeyExists(methodMetadata, "access") && methodMetadata.access == "private") + { + access = "private"; + } + + return access; + } + + + /** Retrieves property and method info about the targetBean. */ + private struct function getTargetBeanMetadata(any beanMetadata) + { + var beanInfo = {accessors = false, methods = {}, name = "", properties = {}}; + var i = 0; + var method = {}; + var property = {}; + var tmpBeanInfo = {}; + + + if (isObject(arguments.beanMetadata)) + { + arguments.beanMetadata = getMetadata(arguments.beanMetadata); + } + + + if (structKeyExists(arguments.beanMetadata, "accessors")) + { + beanInfo.accessors = arguments.beanMetadata.accessors; + } + + + if (structKeyExists(arguments.beanMetadata, "name")) + { + beanInfo.name = arguments.beanMetadata.name; + } + + + // Gather method information. + if (structKeyExists(arguments.beanMetadata, "functions")) + { + // ACF 9 did NOT like using a for-in loop here. + for (i = 1; i <= arrayLen(arguments.beanMetadata.functions); i++) + { + method = arguments.beanMetadata.functions[i]; + beanInfo.methods[method.name] = {}; + + if (structKeyExists(method, "access")) + { + beanInfo.methods[method.name]["access"] = method.access; + } + } + } + + + // Gather property information. + if (structKeyExists(arguments.beanMetadata, "properties")) + { + // ACF 9 did NOT like using a for-in loop here. + for (i = 1; i <= arrayLen(arguments.beanMetadata.properties); i++) + { + property = arguments.beanMetadata.properties[i]; + beanInfo.properties[property.name] = {}; + + if (structKeyExists(property, "access")) + { + beanInfo.properties[property.name]["access"] = property.access; + } + } + } + + + // Handle 'extends' hierarchy info. + if (structKeyExists(arguments.beanMetadata, "extends")) + { + tmpBeanInfo = getTargetBeanMetadata(arguments.beanMetadata.extends); + structAppend(beanInfo.properties, tmpBeanInfo.properties); + structAppend(beanInfo.methods, tmpBeanInfo.methods); + } + + + return beanInfo; + } + + + /** Gathers all the method information for the targetBean. */ + private struct function getTargetBeanMethodInfo() + { + var beanInfo = getTargetBeanMetadata(variables.targetBean); + var key = ""; + var methodInfo = {}; + + + variables.targetBeanPath = beanInfo.name; + + + // Locate methods in 'this' scope of targetBean. + for (key in variables.targetBean) + { + if (!structKeyExists(methodInfo, key) && isCustomFunction(variables.targetBean[key])) + { + methodInfo[key] = {access = "public", discoveredIn = "this", isPropertyAccessor = false}; + } + } + + + // Locate any missing 'set' and 'get' methods only present in the metadata. + if (beanInfo.accessors) + { + for (key in beanInfo.methods) + { + if (!structKeyExists(methodInfo, key)) + { + methodInfo[key] = {access = beanInfo.methods[key].access, discoveredIn = "metadata", isPropertyAccessor = false}; + } + } + } + + + // Determine if any of the 'set' or 'get' methods match a property. + if (beanInfo.accessors) + { + for (key in beanInfo.properties) + { + if (structKeyExists(methodInfo, "set" & key)) + { + methodInfo["set" & key].isPropertyAccessor = true; + } + + + if (structKeyExists(methodInfo, "get" & key)) + { + methodInfo["get" & key].isPropertyAccessor = true; + } + } + } + + + return methodInfo; + } + + + /** Determines if an interceptor has an After method. */ + private boolean function hasAfterMethod(required any interceptor) + { + return structKeyExists(arguments.interceptor.bean, "after"); + } + + + /** Determines if an interceptor has an Around method. */ + private boolean function hasAroundMethod(required any interceptor) + { + return structKeyExists(arguments.interceptor.bean, "around"); + } + + + /** Determines if an interceptor has a Before method. */ + private boolean function hasBeforeMethod(required any interceptor) + { + return structKeyExists(arguments.interceptor.bean, "before"); + } + + + /** Determines if an interceptor has an onError method. */ + private boolean function hasOnErrorMethod(required any interceptor) + { + return structKeyExists(arguments.interceptor.bean, "onError"); + } + + + /** Determines if a 'methodName' is in a list of methods. A blank list of method matches will be an automatic match. */ + private boolean function methodMatches(string methodName, string matchers) + { + // Match on: 1) No matches provided 2) Method name in matchers + return !listLen(arguments.matchers) || arguments.matchers == "*" || listFindNoCase(arguments.matchers, arguments.methodName); + } + + + /** Alters the proxy bean so the factory still sees the set..(), init(), and initMethod() and so these methods get called on the target bean. */ + private void function morphProxy(required struct config) + { + var key = ""; + + // Handle the 'set...' methods. + for (key in variables.targetBean) + { + if (left(key, 3) == "set") + { + this[key] = $passThrough; + } + } + + + // Checks to see if the 'initMethod' was defined in the config and handles if it exists on the target bean. + if (structKeyExists(arguments.config, "initMethod") && len(arguments.config.initMethod) && structKeyExists(variables.targetBean, arguments.config.initMethod)) + { + this[arguments.config.initMethod] = $passThrough; + } + + + // Passes the init() if it exists, otherwise removes it. + if (structKeyExists(variables.targetBean, "init")) + { + this["init"] = $passThrough; + } + else + { + structDelete(this, "init"); + } + } + + + /** Alters the target bean by adding intercept points. */ + private void function morphTargetBean(required struct config) + { + var access = ""; + var beanMethodInfo = getTargetBeanMethodInfo(); + var initMethod = ""; + var key = ""; + var method = ""; + var methodInfo = ""; + + + if (structKeyExists(arguments.config, "initMethod")) + { + initMethod = arguments.config.initMethod; + } + + + variables.targetBean.$inject = _inject; + variables.targetBean.$replaceMethod = $replaceMethod; + + + // Setup internal variables and methods on the target bean. + variables.targetBean.$inject("beanProxy", this, "private"); + variables.targetBean.$inject("preName", variables.preName, "private"); + variables.targetBean.$inject("$callStacks", $callStacks, "private"); + variables.targetBean.$inject("$call", $call); + variables.targetBean.$inject("$methodExists", $methodExists); + variables.targetBean.$inject("$getArgumentInfo", $getArgumentInfo); + variables.targetBean.$inject("interceptID", variables.interceptID); + + + for (key in beanMethodInfo) + { + methodInfo = beanMethodInfo[key]; + + + // Only alter methods that should be intercepted. 'init()', accessors, and 'initMethod' are ignored unless specified in the methods list. + if ( + (variables.interceptedMethods == "*" && key != "init" && key != initMethod && !methodInfo.isPropertyAccessor) || + listFindNoCase(variables.interceptedMethods, key) + ) + { + // Handle methods listed in a scope. + if (listFindNoCase("this,variables", methodInfo.discoveredIn)) + { + // Handle methods found in 'this' scope. + if (methodInfo.access == "public") + { + variables.targetBean.$replaceMethod(key, $callPublicMethod); + } + + // Handle methods in the variables scope. + else + { + variables.targetBean.$replaceMethod(key, $callPublicMethod, "private"); + } + } + + + // Handle methods found only in the metadata. + else + { + try + { + if (methodInfo.access == "public") + { + variables.targetBean.$replaceMethod(key, $callPublicMethod); + } + else + { + variables.targetBean.$replaceMethod(key, $callPublicMethod, "private"); + } + } + catch (any exception) + { + throw(message="Unable to locate the method (" & key & ") on target bean (" & variables.targetBeanPath & ")."); + } + } + } + } + + + structDelete(variables.targetBean, "$inject"); + structDelete(variables.targetBean, "$replaceMethod"); + } + + + /** Adds an array of interceptor definitions to the interceptor definition cache. */ + private void function populateInterceptorCache(required array interceptors) + { + var interceptor = ""; + + for (interceptor in arguments.interceptors) + { + addInterceptor(interceptor); + } + } + + + private any function runAfterStack(string methodName, struct args, any result) + { + if (structKeyExists(arguments, "result") && !isNull(arguments.result)) + { + local.result = arguments.result; + } + + + for (local.interceptor in variables.afterInterceptors) + { + if (methodMatches(methodName, local.interceptor.methods)) + { + local.tempResult = local.interceptor.bean.after(variables.targetBean, arguments.methodName, args, isNull(arguments.result) ? javacast("null", 0) : arguments.result); + } + + if (structKeyExists(local, "tempResult")) + { + if (!isNull(local.tempResult)) + { + local.result = local.tempResult; + } + + structDelete(local, "tempResult"); + } + } + + + if (structKeyExists(local, "result") && !isNull(local.result)) return local.result; + } + + + private any function runAroundStack(string methodName, struct args) + { + if (arrayLen(variables.aroundInterceptors)) + { + // Only need to call the first one in the chain to start the process. + local.result = variables.aroundInterceptors[1].bean.preAround(variables.targetBean, arguments.methodName, arguments.args); + } + else + { + local.result = variables.targetBean.$call(arguments.methodName, arguments.args, true); + } + + if (structKeyExists(local, "result") and !isNull(local.result)) return local.result; + } + + + private void function runBeforeStack(string methodName, struct args) + { + var inteceptor = ""; + + for (inteceptor in variables.beforeInterceptors) + { + if (structKeyExists(inteceptor.bean, "before")) + { + if (methodMatches(arguments.methodName, inteceptor.methods)) + { + inteceptor.bean.before(variables.targetBean, arguments.methodName, arguments.args); + } + } + } + } + + + private void function runOnErrorStack(string methodName, struct args, any exception) + { + var interceptor = ""; + + for (interceptor in variables.errorInterceptors) + { + if (methodMatches(arguments.methodName, interceptor.methods)) + { + interceptor.bean.onError(variables.targetBean, arguments.methodName, arguments.args, arguments.exception); + } + } + } +} diff --git a/framework/cfmljure.cfc b/framework/cfmljure.cfc index 25382080..86c3c780 100644 --- a/framework/cfmljure.cfc +++ b/framework/cfmljure.cfc @@ -1,8 +1,8 @@ component { - variables._fw1_version = "3.5.1"; - variables._cfmljure_version = "1.0.0"; + variables._fw1_version = "4.0.0"; + variables._cfmljure_version = "1.1.0"; /* - Copyright (c) 2012-2015, Sean Corfield + Copyright (c) 2012-2016, Sean Corfield Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -21,35 +21,46 @@ component { // constructor public any function init( string project = "", numeric timeout = 300, string lein = "lein", // to allow default to be overridden + string boot = "", // to allow Boot to be selected instead string ns = "", any root = 0 ) { variables.refCache = { }; + var javaLangSystem = createObject( "java", "java.lang.System" ); + variables.out = javaLangSystem.out; if ( project != "" ) { variables._clj_root = this; variables._clj_ns = ""; - var javaLangSystem = createObject( "java", "java.lang.System" ); - variables.out = javaLangSystem.out; var nl = javaLangSystem.getProperty( "line.separator" ); var fs = javaLangSystem.getProperty( "file.separator" ); var nixLike = fs == "/"; var script = ""; var cmd = { }; var tmpDir = ""; + var buildType = ""; + var buildCommand = ""; + if ( len( boot ) ) { + // select Boot build tool + buildType = "boot"; + buildCommand = boot & " aot show -C"; + } else { + buildType = "lein"; + buildCommand = lein & " with-profile production do clean, compile, classpath"; + } if ( nixLike ) { // *nix / Mac tmpDir = "/tmp"; - script = getTempFile( tmpDir, "lein" ); + script = getTempFile( tmpDir, buildType ); cmd = { cd = "cd", run = "/bin/sh", arg = script, // make sure we are not trying to run under root account! preflightCmd = "if [ `id -u` -eq 0 ]; then >&2 echo 'DO NOT RUN CFML OR CFMLJURE AS ROOT!'; exit 1; fi#nl#", exitCmd = "exit 0#nl#" }; - // ensure Servlet container's options do not affect Leiningen: - lein = "JAVA_OPTS= " & lein; + // ensure Servlet container's options do not affect the build tool: + buildCommand = "JAVA_OPTS= " & buildCommand; } else { // Windows tmpDir = replace( javaLangSystem.getenv( "TEMP" ), chr(92), "/", "all" ); - script = getTempFile( tmpDir, "lein" ); + script = getTempFile( tmpDir, buildType ); script &= ".bat"; cmd = { cd = "chdir", run = script, arg = "", @@ -61,7 +72,7 @@ component { script, "#cmd.cd# #project#" & nl & cmd.preflightCmd & - "#lein# with-profile production do clean, compile, classpath" & nl & + buildCommand & nl & cmd.exitCmd ); var classpath = ""; @@ -77,8 +88,8 @@ component { if ( structKeyExists( URL, "cfmljure" ) && URL.cfmljure == "abortOnFailure" ) { writeDump( var = cmd, label = "Unable to cfexecute this script" ); - if ( !isNull( classpath ) ) writeDump( var = classpath, label = "Leiningen stdout" ); - if ( !isNull( errors ) ) writeDump( var = errors, label = "Leiningen stderr" ); + if ( !isNull( classpath ) ) writeDump( var = classpath, label = "Build (#buildType#) stdout" ); + if ( !isNull( errors ) ) writeDump( var = errors, label = "Build (#buildType#) stderr" ); writeDump( var = e, label = "Full stack trace" ); abort; } @@ -147,7 +158,7 @@ component { this.install = this.__install; this.isAvailable = this.__isAvailable; this.read = this.__read; - var autoLoaded = "clojure.core"; + var autoLoaded = "clojure.core,clojure.walk"; if ( cfmlInteropAvailable ) { variables.out.println( "Detected cfml-interop for interop" ); // perform the best interop we can: @@ -157,7 +168,6 @@ component { } else { variables.out.println( "Falling back to clojure.walk for interop" ); // fall back to basic interop: - autoLoaded = listAppend( autoLoaded, "clojure.walk" ); this.toCFML = this.__toCFML; this.toClojure = this.__toClojure; } @@ -166,7 +176,7 @@ component { variables._clj_root = root; variables._clj_ns = ns; } else { - throw "cfmljure requires the path of a Leiningen project."; + throw "cfmljure requires the path of a Clojure project."; } return this; } @@ -359,6 +369,10 @@ component { } } + public string function __name() { + return variables._clj_ns; + } + public void function __require( string ns ) { if ( !structKeyExists( variables, "_clj_require" ) ) { variables._clj_require = __var( "clojure.core", "require" ); @@ -381,7 +395,11 @@ component { if ( left( missingMethodName, 1 ) == "_" ) { return __( right( missingMethodName, len( missingMethodName ) - 1 ), true ); } else { - return __call( __( missingMethodName, false ), missingMethodArguments ); + var clj_var = __( missingMethodName, false ); + if ( isNull( clj_var ) ) { + throw "Unable to resolve #variables._clj_ns#/#missingMethodName#"; + } + return __call( clj_var, missingMethodArguments ); } } diff --git a/framework/cljcontroller.cfc b/framework/cljcontroller.cfc index 692dfc2d..18c41853 100644 --- a/framework/cljcontroller.cfc +++ b/framework/cljcontroller.cfc @@ -1,21 +1,21 @@ component { - variables._fw1_version = "3.5.1"; - variables._ioclj_version = "1.0.0"; -/* - Copyright (c) 2015, Sean Corfield + variables._fw1_version = "4.0.0"; + variables._ioclj_version = "1.0.1"; + /* + Copyright (c) 2015-2016, Sean Corfield - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ function init( any fw, any cfmljure, any ns ) { variables.fw = fw; @@ -25,20 +25,18 @@ component { } function onMissingMethod( string missingMethodName, struct missingMethodArguments ) { - if ( structKeyExists( missingMethodArguments, "method" ) && - missingMethodArguments.method == "item" ) { + if ( structKeyExists( missingMethodArguments, "rc" ) ) { var rc = missingMethodArguments.rc; try { var rcClj = variables.cfmljure.toClojure( rc ); - var result = variables.cfmljure.toCFML( - evaluate( "variables.ns.#missingMethodName#( rcClj )" ) - ); + var rawResult = callClojure( missingMethodName, rcClj ); + var result = variables.cfmljure.toCFML( rawResult ); structClear( rc ); structAppend( rc, result ); // post-process special keys in rc for abort / redirect etc var core = variables.cfmljure.clojure.core; if ( structKeyExists( rc, "redirect" ) && isStruct( rc.redirect ) && - structKeyExists( rc.redirect, "action" ) ) { + structKeyExists( rc.redirect, "action" ) ) { if ( isObject( variables.fw ) ) { variables.fw.redirect( action = rc.redirect["action"], @@ -52,27 +50,31 @@ component { } } if ( structKeyExists( rc, "render" ) && isStruct( rc.render ) && - structKeyExists( rc.render, "type" ) && structKeyExists( rc.render, "data" ) ) { + structKeyExists( rc.render, "type" ) && structKeyExists( rc.render, "data" ) ) { if ( isObject( variables.fw ) ) { - variables.fw.renderData( + var walk = variables.cfmljure.clojure.walk; + var renderer = variables.fw.renderData( core.name( rc.render["type"] ), - rc.render["data"], - structKeyExists( rc.render, "statusCode" ) ? rc.render["statusCode"] : "200" + // since Clojure generated the render data we must be careful to + // preserve case but still convert keys to strings... + walk.stringify_keys( core.get( core.get( rawResult, core.keyword( "render" ) ), core.keyword( "data" ) ) ) ); + if ( structKeyExists( rc.render, "statusCode" ) ) renderer.statusCode( rc.render["statusCode"] ); + if ( structKeyExists( rc.render, "statusText" ) ) renderer.statusText( rc.render["statusText"] ); } else { throw "Unable to renderData() due to lack of injected FW/1"; } } if ( structKeyExists( rc, "view" ) && isStruct( rc.view ) && - structKeyExists( rc.view, "action" ) ) { + structKeyExists( rc.view, "action" ) ) { if ( isObject( variables.fw ) ) { variables.fw.setView( rc.view["action"] ); } else { - throw "Unable to renderData() due to lack of injected FW/1"; + throw "Unable to setView() due to lack of injected FW/1"; } } if ( structKeyExists( rc, "abort" ) && core.keyword_qmark_( rc.abort ) && - core.name( rc.abort ) == "controller" ) { + core.name( rc.abort ) == "controller" ) { if ( isObject( variables.fw ) ) { variables.fw.abortController(); } else { @@ -82,6 +84,7 @@ component { } catch ( java.lang.IllegalStateException e ) { if ( e.message.startsWith( "Attempting to call unbound fn" ) ) { // no such controller method - ignore it + this[ missingMethodName ] = __dummy; } else { throw e; } @@ -89,4 +92,12 @@ component { } } + function setFramework() { } + + function __dummy() { } + + function callClojure( string qualifiedName, any rcClj ) { + return evaluate( "variables.ns.#qualifiedName#( rcClj )" ); + } + } diff --git a/framework/facade.cfc b/framework/facade.cfc new file mode 100644 index 00000000..50026271 --- /dev/null +++ b/framework/facade.cfc @@ -0,0 +1,30 @@ +component { + variables._fw1_version = "4.0.0"; +/* + Copyright (c) 2016, Sean Corfield + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + + function init() { + try { + return request._fw1.theFramework; + } catch ( any e ) { + throw( + type = "FW1.FacadeException", message = "Unable to locate FW/1 for this request", + detail = "It appears that you asked for the facade in a request that did not originate in FW/1?" + ); + } + } + +} diff --git a/framework/ioc.cfc b/framework/ioc.cfc index 812771d1..b02016c2 100644 --- a/framework/ioc.cfc +++ b/framework/ioc.cfc @@ -1,8 +1,8 @@ component { - variables._fw1_version = "3.5.1"; - variables._di1_version = "1.1.1"; + variables._fw1_version = "4.0.0"; + variables._di1_version = "1.2.0"; /* - Copyright (c) 2010-2015, Sean Corfield + Copyright (c) 2010-2016, Sean Corfield Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -29,12 +29,22 @@ component { } var n = arrayLen( variables.folderArray ); for ( var i = 1; i <= n; ++i ) { - variables.folderArray[ i ] = trim( variables.folderArray[ i ] ); + var folderName = trim( variables.folderArray[ i ] ); + // strip trailing slash since it can cause weirdness in path + // deduction on some engines on some platforms (guess which!) + if ( len( folderName ) > 1 && + ( right( folderName, 1 ) == '/' || + right( folderName, 1 ) == chr(92) ) ) { + folderName = left( folderName, len( folderName ) - 1 ); + } + variables.folderArray[ i ] = folderName; } variables.config = config; variables.beanInfo = { }; variables.beanCache = { }; variables.resolutionCache = { }; + variables.getBeanCache = { }; + variables.accumulatorCache = { }; variables.initMethodCache = { }; variables.settersInfo = { }; variables.autoExclude = [ @@ -64,7 +74,7 @@ component { // programmatically register an alias public any function addAlias( string aliasName, string beanName ) { - discoverBeans(); + discoverBeans(); // still need this since we rely on beanName having been discovered :( variables.beanInfo[ aliasName ] = variables.beanInfo[ beanName ]; return this; } @@ -72,7 +82,6 @@ component { // programmatically register new beans with the factory (add a singleton name/value pair) public any function addBean( string beanName, any beanValue ) { - discoverBeans(); variables.beanInfo[ beanName ] = { name = beanName, value = beanValue, isSingleton = true }; @@ -84,13 +93,82 @@ component { public boolean function containsBean( string beanName ) { discoverBeans(); return structKeyExists( variables.beanInfo, beanName ) || - ( structKeyExists( variables, 'parent' ) && variables.parent.containsBean( beanName ) ); + ( hasParent() && variables.parent.containsBean( beanName ) ); + } + + + // builder syntax for declaring new beans + public any function declare( string beanName ) { + var declaration = { beanName : beanName, built : false }; + var beanFactory = this; // to make the builder functions less confusing + structAppend( declaration, { + // builder for addAlias() + aliasFor : function( string beanName ) { + if ( declaration.built ) throw "Declaration builder already completed!"; + declaration.built = true; + beanFactory.addAlias( declaration.beanName, beanName ); + return declaration; + }, + // builder for addBean() + asValue : function( any beanValue ) { + if ( declaration.built ) throw "Declaration builder already completed!"; + declaration.built = true; + beanFactory.addBean( declaration.beanName, beanValue ); + return declaration; + }, + // builder for factoryBean() + fromFactory : function( any factory, string methodName = "" ) { + if ( declaration.built ) throw "Declaration builder already completed!"; + declaration.built = true; + // use defaults -- we can override later + beanFactory.factoryBean( declaration.beanName, factory, methodName ); + return declaration; + }, + // builder for declareBean() + instanceOf : function( string dottedPath ) { + if ( declaration.built ) throw "Declaration builder already completed!"; + declaration.built = true; + // use defaults -- we can override later + beanFactory.declareBean( declaration.beanName, dottedPath ); + return declaration; + }, + // modifiers for metadata + asSingleton : function() { + if ( !declaration.built ) throw "No declaration builder to modify!"; + variables.beanInfo[ declaration.beanName ].isSingleton = true; + return declaration; + }, + asTransient : function() { + if ( !declaration.built ) throw "No declaration builder to modify!"; + variables.beanInfo[ declaration.beanName ].isSingleton = false; + return declaration; + }, + withArguments : function( array args ) { + if ( !declaration.built ) throw "No declaration builder to modify!"; + var info = variables.beanInfo[ declaration.beanName ]; + if ( !structKeyExists( info, 'factory' ) ) throw "withArguments() requires fromFactory()!"; + info.args = args; + return declaration; + }, + withOverrides : function( struct overrides ) { + if ( !declaration.built ) throw "No declaration builder to modify!"; + var info = variables.beanInfo[ declaration.beanName ]; + if ( !structKeyExists( info, 'factory' ) && + !structKeyExists( info, 'cfc' ) ) throw "withOverrides() requires fromFactory() or instanceOf()!"; + info.overrides = overrides; + return declaration; + }, + // to allow chaining + done : function() { + return beanFactory; + } + } ); + return declaration; } // programmatically register new beans with the factory (add an actual CFC) public any function declareBean( string beanName, string dottedPath, boolean isSingleton = true, struct overrides = { } ) { - discoverBeans(); var singleDir = ''; if ( listLen( dottedPath, '.' ) > 1 ) { var cfc = listLast( dottedPath, '.' ); @@ -113,8 +191,7 @@ component { return this; } - public any function factoryBean( string beanName, any factory, string methodName, array args = [ ], struct overrides = { } ) { - discoverBeans(); + public any function factoryBean( string beanName, any factory, string methodName = "", array args = [ ], struct overrides = { } ) { var metadata = { name = beanName, isSingleton = false, // really? factory = factory, method = methodName, args = args, @@ -126,14 +203,22 @@ component { // return the requested bean, fully populated - public any function getBean( string beanName ) { + public any function getBean( string beanName, struct constructorArgs = { } ) { discoverBeans(); if ( structKeyExists( variables.beanInfo, beanName ) ) { - return resolveBean( beanName ); - } else if ( structKeyExists( variables, 'parent' ) ) { - return variables.parent.getBean( beanName ); + if ( structKeyExists( variables.getBeanCache, beanName ) ) { + return variables.getBeanCache[ beanName ]; + } + var bean = resolveBean( beanName, constructorArgs ); + if ( isSingleton( beanName ) ) variables.getBeanCache[ beanName ] = bean; + return bean; + } else if ( hasParent() ) { + // ideally throw an exception for non-DI/1 parent when args passed + // WireBox adapter can do that since we control it but we can't do + // anything for other bean factories - will revisit before release + return variables.parent.getBean( beanName, constructorArgs ); } else { - throw 'bean not found: #beanName#'; + return missingBean( beanName = beanName, dependency = false ); } } @@ -146,13 +231,13 @@ component { if ( structKeyExists( variables.beanInfo, beanName ) ) { return variables.beanInfo[ beanName ]; } - if ( structKeyExists( variables, 'parent' ) ) { + if ( hasParent() ) { return parentBeanInfo( beanName ); } throw 'bean not found: #beanName#'; } else { var result = { beanInfo = { } }; - if ( structKeyExists( variables, 'parent' ) ) { + if ( hasParent() ) { if ( flatten || len( regex ) ) { structAppend( result.beanInfo, parentBeanInfoList( flatten ).beanInfo ); structAppend( result.beanInfo, variables.beanInfo ); @@ -190,12 +275,18 @@ component { } + // return true if this factory has a parent + public boolean function hasParent() { + return structKeyExists( variables, 'parent' ); + } + + // return true iff bean is known to be a singleton public boolean function isSingleton( string beanName ) { discoverBeans(); if ( structKeyExists( variables.beanInfo, beanName ) ) { return variables.beanInfo[ beanName ].isSingleton; - } else if ( structKeyExists( variables, 'parent' ) ) { + } else if ( hasParent() ) { try { return variables.parent.isSingleton( beanName ); } catch ( any e ) { @@ -234,6 +325,8 @@ component { discoverBeans(); variables.beanCache = { }; variables.resolutionCache = { }; + variables.accumulatorCache = { }; + variables.getBeanCache = { }; variables.initMethodCache = { }; for ( var key in variables.beanInfo ) { if ( !structKeyExists( variables.beanInfo[ key ], "isSingleton" ) ) @@ -318,8 +411,12 @@ component { ( !structKeyExists( property, 'setter' ) || isBoolean( property.setter ) && property.setter ) ) { if ( structKeyExists( property, 'type' ) && - property.type != 'any' ) { - iocMeta.setters[ property.name ] = 'typed'; + property.type != 'any' && + variables.config.omitTypedProperties ) { + iocMeta.setters[ property.name ] = 'ignored'; + } else if ( structKeyExists( property, 'default' ) && + variables.config.omitDefaultedProperties ) { + iocMeta.setters[ property.name ] = 'ignored'; } else { iocMeta.setters[ property.name ] = 'implicit'; } @@ -558,8 +655,12 @@ component { } - private void function missingBean( string beanName, string resolvingBeanName = '' ) { - if ( variables.config.strict ) { + /* + * override this if you want to add a convention-based bean factory hook, that returns + * beans instead of throwing an exception + */ + private any function missingBean( string beanName, string resolvingBeanName = '', boolean dependency = true ) { + if ( variables.config.strict || !dependency ) { if ( len( resolvingBeanName ) ) { throw 'bean not found: #beanName#; while resolving #resolvingBeanName#'; } else { @@ -574,9 +675,7 @@ component { private void function onLoadEvent() { var head = variables.listeners; while ( isStruct( head ) ) { - if ( isCustomFunction( head.listener ) || - ( listFirst( server.coldfusion.productVersion ) >= 10 && - isClosure( head.listener ) ) ) { + if ( isCustomFunction( head.listener ) || isClosure( head.listener ) ) { head.listener( this ); } else if ( isObject( head.listener ) ) { head.listener.onLoad( this ); @@ -621,11 +720,14 @@ component { } - private any function resolveBean( string beanName ) { + private any function resolveBean( string beanName, struct constructorArgs = { } ) { // do enough resolution to create and initialization this bean // returns a struct of the bean and a struct of beans and setters still to run // construction phase: - var partialBean = resolveBeanCreate( beanName, { injection = { }, dependencies = { } } ); + if ( !structKeyExists( variables.accumulatorCache, beanName ) ) { + variables.accumulatorCache[ beanName ] = { injection = { }, dependencies = { } }; + } + var partialBean = resolveBeanCreate( beanName, variables.accumulatorCache[ beanName ], constructorArgs ); if ( structKeyExists( variables.resolutionCache, beanName ) && variables.resolutionCache[ beanName ] ) { // fully resolved, no action needed this time @@ -641,9 +743,8 @@ component { postInjectables[ name ] = true; } for ( var property in injection.setters ) { - if ( injection.setters[ property ] == 'typed' && - variables.config.omitTypedProperties ) { - // we do not inject typed properties! + if ( injection.setters[ property ] == 'ignored' ) { + // do not inject defaulted/typed properties! continue; } var args = { }; @@ -651,11 +752,13 @@ component { args[ property ] = injection.overrides[ property ]; } else if ( structKeyExists( partialBean.injection, property ) ) { args[ property ] = partialBean.injection[ property ].bean; - } else if ( structKeyExists( variables, 'parent' ) && variables.parent.containsBean( property ) ) { + } else if ( hasParent() && variables.parent.containsBean( property ) ) { args[ property ] = variables.parent.getBean( property ); } else { - missingBean( property, beanName ); - continue; + // allow for possible convention-based bean factory + args[ property ] = missingBean( property, beanName ); + // isNull() does not always work on ACF10... + try { if ( isNull( args[ property ] ) ) continue; } catch ( any e ) { continue; } } evaluate( 'injection.bean.set#property#( argumentCollection = args )' ); } @@ -699,15 +802,26 @@ component { } - private struct function resolveBeanCreate( string beanName, struct accumulator ) { + private struct function resolveBeanCreate( string beanName, struct accumulator, struct constructorArgs = { } ) { var bean = 0; if ( structKeyExists( variables.beanInfo, beanName ) ) { var info = variables.beanInfo[ beanName ]; - accumulator.dependencies[ beanName ] = { }; + if ( !structKeyExists( accumulator.dependencies, beanName ) ) accumulator.dependencies[ beanName ] = { }; if ( structKeyExists( info, 'cfc' ) ) { /*******************************************************/ var metaBean = cachable( beanName ); - var overrides = structKeyExists( info, 'overrides' ) ? info.overrides : { }; + var overrides = { }; + // be careful not to modify overrides metadata: + if ( structCount( constructorArgs ) ) { + if ( structKeyExists( info, 'overrides' ) ) { + structAppend( overrides, info.overrides ); + } + structAppend( overrides, constructorArgs ); + } else { + if ( structKeyExists( info, 'overrides' ) ) { + overrides = info.overrides; + } + } bean = metaBean.bean; if ( metaBean.newObject ) { if ( structKeyExists( info.metadata, 'constructor' ) ) { @@ -776,9 +890,11 @@ component { } } } - accumulator.bean = bean; + if ( !isSingleton( beanName ) && structKeyExists( accumulator.injection, beanName ) ) { + accumulator.injection[ beanName ].bean = bean; + } } else if ( isConstant( beanName ) ) { - accumulator.bean = info.value; + bean = info.value; accumulator.injection[ beanName ] = { bean = info.value, setters = { } }; } else if ( structKeyExists( info, 'factory' ) ) { var fmBean = isSimpleValue( info.factory ) ? this.getBean( info.factory ) : info.factory; @@ -792,19 +908,30 @@ component { argStruct[ i ] = this.getBean( argName ); } } - accumulator.bean = evaluate( 'fmBean.#info.method#(argumentCollection=argStruct)' ); - accumulator.injection[ beanName ] = { bean = accumulator.bean, setters = { } }; + if ( isCustomFunction( fmBean ) || isClosure( fmBean ) ) { + bean = fmBean( argumentCollection = argStruct ); + } else { + bean = evaluate( 'fmBean.#info.method#( argumentCollection = argStruct )' ); + } + accumulator.injection[ beanName ] = { bean = bean, setters = { } }; } else { throw 'internal error: invalid metadata for #beanName#'; } - } else if ( structKeyExists( variables, 'parent' ) && variables.parent.containsBean( beanName ) ) { - bean = variables.parent.getBean( beanName ); - accumulator.injection[ beanName ] = { bean = bean, setters = { } }; - accumulator.bean = bean; } else { - missingBean( beanName ); + if ( hasParent() && variables.parent.containsBean( beanName ) ) { + bean = variables.parent.getBean( beanName ); + } else { + bean = missingBean( beanName = beanName, dependency = true ); + } + if ( !isNull( bean ) ) { + accumulator.injection[ beanName ] = { bean = bean, setters = { } }; + } } - return accumulator; + return { + bean = bean, + injection = accumulator.injection, + dependencies = accumulator.dependencies + }; } @@ -839,8 +966,11 @@ component { throw 'singletonPattern and transientPattern are mutually exclusive'; } + if ( !structKeyExists( variables.config, 'omitDefaultedProperties' ) ) { + variables.config.omitDefaultedProperties = true; + } if ( !structKeyExists( variables.config, 'omitTypedProperties' ) ) { - variables.config.omitTypedProperties = false; + variables.config.omitTypedProperties = true; } if ( !structKeyExists( variables.config, 'omitDirectoryAliases' ) ) { variables.config.omitDirectoryAliases = false; diff --git a/framework/ioclj.cfc b/framework/ioclj.cfc index 2014c990..ebc5af8f 100644 --- a/framework/ioclj.cfc +++ b/framework/ioclj.cfc @@ -1,8 +1,8 @@ component extends=framework.ioc { - variables._fw1_version = "3.5.1"; - variables._ioclj_version = "1.0.0"; + variables._fw1_version = "4.0.0"; + variables._ioclj_version = "1.1.0"; /* - Copyright (c) 2015, Sean Corfield + Copyright (c) 2015-2016, Sean Corfield Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -24,12 +24,31 @@ component extends=framework.ioc { if ( variables.debug ) { variables.stdout = createObject( "java", "java.lang.System" ).out; } + if ( isSimpleValue( folders ) ) { + folders = listToArray( folders ); + } + var cfmlFolders = [ ]; + var cljFolders = [ ]; + for ( var folder in folders ) { + if ( len( folder ) > 4 && left( folder, 4 ) == "clj:" ) { + arrayAppend( cljFolders, right( folder, len( folder ) - 4 ) ); + } else if ( len( folder ) > 5 && left( folder, 5 ) == "cfml:" ) { + arrayAppend( cfmlFolders, right( folder, len( folder ) - 5 ) ); + } else { + arrayAppend( cfmlFolders, folder ); + arrayAppend( cljFolders, folder ); + } + } + variables.cljFolderArray = cljFolders; // initialize DI/1 parent - super.init( folders, config ); + super.init( cfmlFolders, config ); variables.cljBeans = { }; if ( structKeyExists( config, "noClojure" ) && config.noClojure ) return; - // find the first folder that includes project.clj - that's our project - variables.project = findProjectFile(); + var lein = structKeyExists( config, "lein" ) ? config.lein : "lein"; + var boot = structKeyExists( config, "boot" ) ? config.boot : ""; // default is not Boot + var cljcontroller = structKeyExists( config, "cljcontroller" ) ? config.cljcontroller : "framework.cljcontroller"; + // find the first folder that includes project.clj (or build.boot) - that's our project + variables.project = findProjectFile( len( boot ) ? "build.boot" : "project.clj" ); discoverClojureFiles(); // list of namespaces to expose: var ns = [ ]; @@ -38,16 +57,15 @@ component extends=framework.ioc { } // and create a cfmljure instance var timeout = structKeyExists( config, "timeout" ) ? config.timeout : 300; - var lein = structKeyExists( config, "lein" ) ? config.lein : "lein"; var useServerScope = structKeyExists( config, "server" ) ? config.server : false; var cfmljure = 0; if ( useServerScope ) { if ( !structKeyExists( server, "__cfmljure" ) ) { - server.__cfmljure = new framework.cfmljure( variables.project, timeout, lein ); + server.__cfmljure = new framework.cfmljure( variables.project, timeout, lein, boot ); } cfmljure = server.__cfmljure; } else { - cfmljure = new framework.cfmljure( variables.project, timeout, lein ); + cfmljure = new framework.cfmljure( variables.project, timeout, lein, boot ); } if ( cfmljure.isAvailable() ) { // Clojure loaded -- install the discovered namespaces @@ -63,8 +81,26 @@ component extends=framework.ioc { // patch DI/1 bean info to include Clojure "beans" -- this allows Clojure // to be autowired like any other "value" bean: for ( var cljBean in variables.cljBeans ) { + var info = variables.cljBeans[ cljBean ]; + // navigate to actual namespace "object": + var ns = variables.clojureApp; + for ( var x in info.nsx ) { + ns = ns[ x ]; + } + var bean = ns; + if ( info.type == "controller" ) { + // need a wrapper - try to find FW/1 instance via bean factory: + var fw = containsBean( "fw" ) ? getBean( "fw" ) : + ( containsBean( "fw1" ) ? getBean( "fw1" ) : + ( containsBean( "framework" ) ? getBean( "framework" ) : + "" ) ); + var controller = new "#cljcontroller#"( + fw, variables.cfmljure, ns + ); + bean = controller; + } variables.beanInfo[ cljBean ] = { - name : cljBean, value : getBean( cljBean ), isSingleton : true + name : cljBean, value : bean, isSingleton : true }; } // add cfmljure to expose Clojure-related functions: @@ -75,40 +111,6 @@ component extends=framework.ioc { // PUBLIC METHODS - // return true if the factory (or a parent factory) knows about the requested bean - public boolean function containsBean( string beanName ) { - return structKeyExists( variables.cljBeans, beanName ) || super.containsBean( beanName ); - } - - // return the requested bean, fully populated - public any function getBean( string beanName ) { - if ( structKeyExists( variables.cljBeans, beanName ) ) { - var info = variables.cljBeans[ beanName ]; - // navigate to actual namespace "object": - var ns = variables.clojureApp; - for ( var x in info.nsx ) { - ns = ns[ x ]; - } - if ( info.type == "controller" ) { - // need a wrapper - try to find FW/1 instance via bean factory: - var fw = super.containsBean( "fw" ) ? super.getBean( "fw" ) : - ( super.containsBean( "fw1" ) ? super.getBean( "fw1" ) : - ( super.containsBean( "framework" ) ? super.getBean( "framework" ) : - "" ) ); - var controller = new framework.cljcontroller( - fw, variables.cfmljure, ns - ); - return controller; - } else { - // expose as a regular bean - return ns; - } - } else { - return super.getBean( beanName ); - } - } - - // convenience API for metaprogramming perhaps? public any function getBeanInfo( string beanName = '', boolean flatten = false, string regex = '' ) { @@ -119,8 +121,13 @@ component extends=framework.ioc { } return super.getBeanInfo( beanName, flatten, regex ); } else { - var result = super.getBeanInfo( beanName, flatten, regex ); + var result = { beanInfo = { } }; + var superInfo = super.getBeanInfo( beanName, flatten, regex ); structAppend( result.beanInfo, variables.cljBeans ); + structAppend( result.beanInfo, superInfo.beanInfo ); + if ( structKeyExists( superInfo, 'parent' ) ) { + result.parent = superInfo.parent; + } if ( len( regex ) ) { var matched = { }; for ( var name in result.beanInfo ) { @@ -153,62 +160,67 @@ component extends=framework.ioc { private void function discoverClojureFiles() { var cljs = [ ]; - var src = variables.project & "/src"; - var n = len( src ) + 1; // allow for trailing / - try { - cljs = directoryList( src, true, "path", "*.clj" ); - // we also support .cljc files - var cljcs = directoryList( src, true, "path", "*.cljc" ); - for ( var cljcOSPath in cljcs ) cljs.append( cljcOSPath ); - } catch ( any e ) { - // assume bad path and ignore it - } - for ( var cljOSPath in cljs ) { - var cljPath = replace( cljOSPath, chr(92), "/", "all" ); - cljPath = right( cljPath, len( cljPath ) - n ); - // allow for extension being either .clj or .cljc - cljPath = left( cljPath, len( cljPath ) - ( right( cljPath, 1 ) == "c" ? 5 : 4 ) ); - var ns = replace( replace( cljPath, "/", ".", "all" ), "_", "-", "all" ); - // per #366, the pattern allowed is - // top-level(.optional)*.plural.(prefix.)*name - // and this will generate prefixNameSingular - var parts = listToArray( cljPath, "/" ); - var nParts = arrayLen( parts ); - // ignore temp files from editors (starting with .) - if ( left( parts[ nParts ], 1 ) == "." ) continue; - if ( nParts >= 3 ) { - var pluralCandidate = 2; - do { - var lbo = parts[ pluralCandidate ]; - var lbo1 = singular( lbo ); - ++pluralCandidate; - } while ( lbo == lbo1 && pluralCandidate < nParts ); - if ( lbo1 != lbo ) { - var beanName = ""; - while ( pluralCandidate <= nParts ) { - beanName &= parts[ pluralCandidate ]; + for ( var folder in variables.cljFolderArray ) { + var src = folder & "/src"; + var expandedFolder = expandPath( src ); + if ( directoryExists( expandedFolder ) ) src = expandedFolder; + if ( !directoryExists( src ) ) continue; + var n = len( src ) + 1; // allow for trailing / + try { + cljs = directoryList( src, true, "path", "*.clj" ); + // we also support .cljc files + var cljcs = directoryList( src, true, "path", "*.cljc" ); + for ( var cljcOSPath in cljcs ) cljs.append( cljcOSPath ); + } catch ( any e ) { + // assume bad path and ignore it + } + for ( var cljOSPath in cljs ) { + var cljPath = replace( cljOSPath, chr(92), "/", "all" ); + cljPath = right( cljPath, len( cljPath ) - n ); + // allow for extension being either .clj or .cljc + cljPath = left( cljPath, len( cljPath ) - ( right( cljPath, 1 ) == "c" ? 5 : 4 ) ); + var ns = replace( replace( cljPath, "/", ".", "all" ), "_", "-", "all" ); + // per #366, the pattern allowed is + // top-level(.optional)*.plural.(prefix.)*name + // and this will generate prefixNameSingular + var parts = listToArray( cljPath, "/" ); + var nParts = arrayLen( parts ); + // ignore temp files from editors (starting with .) + if ( left( parts[ nParts ], 1 ) == "." ) continue; + if ( nParts >= 3 ) { + var pluralCandidate = 2; + do { + var lbo = parts[ pluralCandidate ]; + var lbo1 = singular( lbo ); ++pluralCandidate; - } - beanName &= lbo1; - if ( structKeyExists( variables.cljBeans, beanName ) ) { - throw "#beanName# is not unique (from #cljPath#)"; - } else { - variables.cljBeans[ beanName ] = { - ns : ns, nsx : parts, type : lbo1, - isSingleton : true // for DI/1 compatibility - }; + } while ( lbo == lbo1 && pluralCandidate < nParts ); + if ( lbo1 != lbo ) { + var beanName = ""; + while ( pluralCandidate <= nParts ) { + beanName &= parts[ pluralCandidate ]; + ++pluralCandidate; + } + beanName &= lbo1; + if ( structKeyExists( variables.cljBeans, beanName ) ) { + throw "#beanName# is not unique (from #cljPath#)"; + } else { + variables.cljBeans[ beanName ] = { + ns : ns, nsx : parts, type : lbo1, + isSingleton : true // for DI/1 compatibility + }; + } + } else if ( variables.debug ) { + variables.stdout.println( "ioclj: ignoring #cljPath#.clj because it has no plural segment" ); } } else if ( variables.debug ) { - variables.stdout.println( "ioclj: ignoring #cljPath#.clj because it has no plural segment" ); + variables.stdout.println( "ioclj: ignoring #cljPath#.clj because it does not have at least three segments" ); } - } else if ( variables.debug ) { - variables.stdout.println( "ioclj: ignoring #cljPath#.clj because it does not have at least three segments" ); } } } - private string function findProjectFile() { - for ( var folder in variables.folderArray ) { + private string function findProjectFile( string buildFile ) { + for ( var folder in variables.cljFolderArray ) { if ( right( folder, 1 ) == "/" ) { if ( len( folder ) == 1 ) folder = ""; else folder = left( folder, len( folder ) - 1 ); @@ -222,13 +234,13 @@ component extends=framework.ioc { if ( len( path ) == 1 ) path = ""; else path = left( path, len( path ) - 1 ); } - if ( fileExists( path & "/project.clj" ) ) { + if ( fileExists( path & "/" & buildFile ) ) { // found our Clojure project, return it - if ( variables.debug ) variables.stdout.println( "ioclj: using #path#/project.clj for Clojure root" ); + if ( variables.debug ) variables.stdout.println( "ioclj: using #path#/#buildFile# for Clojure root" ); return path; } } - throw "Unable to find project.clj in any of: #variables.folderList#"; + throw "Unable to find #buildFile# in any of: #arrayToList( variables.cljFolderArray )#"; } } diff --git a/framework/one.cfc b/framework/one.cfc index da13036d..0b7901b4 100644 --- a/framework/one.cfc +++ b/framework/one.cfc @@ -1,20 +1,20 @@ component { - variables._fw1_version = "3.5.1"; -/* - Copyright (c) 2009-2015, Sean Corfield, Marcin Szczepanski, Ryan Cogswell + variables._fw1_version = "4.0.0"; + /* + Copyright (c) 2009-2016, Sean Corfield, Marcin Szczepanski, Ryan Cogswell - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ this.name = hash( getBaseTemplatePath() ); if ( !structKeyExists( request, '_fw1' ) ) { @@ -24,6 +24,7 @@ component { cgiRequestMethod = CGI.REQUEST_METHOD, controllers = [ ], requestDefaultsInitialized = false, + routeMethodsMatched = { }, doTrace = false, trace = [ ] }; @@ -251,7 +252,7 @@ component { if ( structKeyExists( request._fw1, 'controllerExecutionStarted' ) ) { throw( 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.' ); + detail='The controller execution phase has already started. Controllers may not be added by other controller methods.' ); } tuple.controller = getController( section = section, subsystem = subsystem ); @@ -391,7 +392,7 @@ component { if ( variables.framework.defaultSubsystem == '' ) { throw( 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.' ); + detail='When using subsystems, every request should specify a subsystem or variables.framework.defaultSubsystem should be configured.' ); } return variables.framework.defaultSubsystem; @@ -466,14 +467,28 @@ component { return listLast( getSectionAndItem( action ), '.' ); } + /* + * return this request's CGI method + */ + public string function getCGIRequestMethod() { + return request._fw1.cgiRequestMethod; + } /* * return the current route (if any) + * this is the raw, matched route that we mapped */ public string function getRoute() { return structKeyExists( request._fw1, 'route' ) ? request._fw1.route : ''; } + /* + * return the part of the pathinfo that was used as the route + * prefixed by the HTTP method + */ + public string function getRoutePath() { + return '$' & request._fw1.cgiRequestMethod & request._fw1.currentRoute; + } /* * return the configured routes @@ -700,9 +715,9 @@ component { try { if ( !structKeyExists( variables, 'framework' ) || !structKeyExists( variables.framework, 'version' ) ) { - // error occurred before framework was initialized - failure( exception, event, false, true ); - return; + // error occurred before framework was initialized + failure( exception, event, false, true ); + return; } // record details of the exception: @@ -713,7 +728,6 @@ component { request.event = event; // reset lifecycle flags: structDelete( request, 'layout' ); - structDelete( request._fw1, 'controllerExecutionComplete' ); structDelete( request._fw1, 'controllerExecutionStarted' ); structDelete( request._fw1, 'overrideViewAction' ); if ( structKeyExists( request._fw1, 'renderData' ) ) { @@ -770,10 +784,10 @@ component { * this can be overridden if you want to change the behavior when * FW/1 cannot find a matching view */ - public string function onMissingView( struct rc ) { + public any 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 + // if we got here, we would return the string or struct to be rendered // but viewNotFound() throws an exception... // for example, return view( 'main.missing' ); } @@ -803,32 +817,45 @@ component { var n = 0; request._fw1.controllerExecutionStarted = true; - try { - 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 ( variables.framework.preflightOptions && + request._fw1.cgiRequestMethod == "OPTIONS" && + structCount( request._fw1.routeMethodsMatched ) ) { + // OPTIONS support enabled and at least one possible match + // bypass all normal controllers and render headers and data: + var resp = getPageContext().getResponse(); + resp.setHeader( "Access-Control-Allow-Origin", variables.framework.optionsAccessControl.origin ); + resp.setHeader( "Access-Control-Allow-Methods", "OPTIONS," & uCase( structKeyList( request._fw1.routeMethodsMatched ) ) ); + resp.setHeader( "Access-Control-Allow-Headers", variables.framework.optionsAccessControl.headers ); + resp.setHeader( "Access-Control-Allow-Credentials", variables.framework.optionsAccessControl.credentials ? "true" : "false" ); + resp.setHeader( "Access-Control-Max-Age", "#variables.framework.optionsAccessControl.maxAge#" ); + renderData( "text", "" ); + } else { + try { + 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, tuple.item, 'item' ); if ( structKeyExists( request._fw1, 'abortController' ) ) abortController(); } - doController( tuple, tuple.item, 'item' ); - if ( structKeyExists( request._fw1, 'abortController' ) ) abortController(); - } - n = arrayLen( request._fw1.controllers ); - for ( i = n; i >= 1; i = i - 1 ) { - tuple = request._fw1.controllers[ i ]; - // run after once per controller (in reverse order): - if ( once[ tuple.key ] eq i ) { - doController( tuple, 'after', 'after' ); - if ( structKeyExists( request._fw1, 'abortController' ) ) abortController(); + n = arrayLen( request._fw1.controllers ); + for ( i = n; i >= 1; i = i - 1 ) { + tuple = request._fw1.controllers[ i ]; + // run after once per controller (in reverse order): + if ( once[ tuple.key ] eq i ) { + doController( tuple, 'after', 'after' ); + if ( structKeyExists( request._fw1, 'abortController' ) ) abortController(); + } } + } catch ( FW1.AbortControllerException e ) { + // do "nothing" since this is a control flow exception } - } catch ( FW1.AbortControllerException e ) { - // do "nothing" since this is a control flow exception } - request._fw1.controllerExecutionComplete = true; if ( structKeyExists( request._fw1, 'renderData' ) ) { out = renderDataWithContentType(); @@ -858,7 +885,19 @@ component { out = internalLayout( request._fw1.layouts[i], out ); } } - writeOutput( out ); + if ( isSimpleValue( out ) ) { + writeOutput( out ); + } else { + if ( structKeyExists( out, 'contentType' ) ) { + var resp = getPageContext().getResponse(); + resp.setContentType( out.contentType ); + } + if ( structKeyExists( out, 'writer' ) ) { + out.writer( out.output ); + } else { + writeOutput( out.output ); + } + } setupResponseWrapper(); } @@ -882,7 +921,7 @@ component { if ( !isFrameworkInitialized() || isFrameworkReloadRequest() ) { setupApplicationWrapper(); } else { - variables.fw1App = getFw1App(); + request._fw1.theApp = getFw1App(); } restoreFlashContext(); @@ -1109,8 +1148,60 @@ component { } // call this to render data rather than a view and layouts - public void function renderData( string type, any data, numeric statusCode = 200, string jsonpCallback = "" ) { - request._fw1.renderData = { type = type, data = data, statusCode = statusCode, jsonpCallback = jsonpCallback }; + // arguments are deprecated in favor of build syntax as of 4.0 + public any function renderData( string type = '', any data = '', numeric statusCode = 200, string jsonpCallback = "" ) { + if ( statusCode != 200 ) deprecated( false, "Use the .statusCode() builder syntax instead of the inline argument." ); + if ( len( jsonpCallback ) ) deprecated( false, "Use the .jsonpCallback() builder syntax instead of the inline argument." ); + request._fw1.renderData = { + type = type, + data = data, + statusCode = statusCode, + statusText = '', + jsonpCallback = jsonpCallback + }; + // return a builder to support nicer rendering syntax + return renderer(); + } + + public any function renderer() { + var builder = { }; + structAppend( builder, { + // allow type and data to be overridden just for completeness + type : function( v ) { + if ( !structKeyExists( request._fw1, 'renderData' ) ) request._fw1.renderData = { }; + request._fw1.renderData.type = v; + return builder; + }, + data : function( v ) { + if ( !structKeyExists( request._fw1, 'renderData' ) ) request._fw1.renderData = { }; + request._fw1.renderData.data = v; + return builder; + }, + header : function( h, v ) { + if ( !structKeyExists( request._fw1, 'renderData' ) ) request._fw1.renderData = { }; + if ( !structKeyExists( request._fw1.renderData, 'headers' ) ) { + request._fw1.renderData.headers = [ ]; + } + arrayAppend( request._fw1.renderData.headers, { name = h, value = v } ); + return builder; + }, + statusCode : function( v ) { + if ( !structKeyExists( request._fw1, 'renderData' ) ) request._fw1.renderData = { }; + request._fw1.renderData.statusCode = v; + return builder; + }, + statusText : function( v ) { + if ( !structKeyExists( request._fw1, 'renderData' ) ) request._fw1.renderData = { }; + request._fw1.renderData.statusText = v; + return builder; + }, + jsonpCallback : function( v ) { + if ( !structKeyExists( request._fw1, 'renderData' ) ) request._fw1.renderData = { }; + request._fw1.renderData.jsonpCallback = v; + return builder; + } + } ); + return builder; } /* @@ -1237,8 +1328,8 @@ component { * view() may be invoked inside views and layouts * returns the UI generated by the named view */ - public string function view( string path, struct args = { }, - any missingView = { } ) { + public any function view( string path, struct args = { }, + any missingView = { } ) { var viewPath = parseViewOrLayoutPath( path, 'view' ); if ( cachedFileExists( viewPath ) ) { internalFrameworkTrace( 'view( #path# ) called - rendering #viewPath#' ); @@ -1298,7 +1389,7 @@ component { internalFrameworkTrace( 'building layout queue', subsystem, section, item ); // look for item-specific layout: testLayout = parseViewOrLayoutPath( subsystem & variables.framework.subsystemDelimiter & - section & '/' & item, 'layout' ); + section & '/' & item, 'layout' ); if ( cachedFileExists( testLayout ) ) { internalFrameworkTrace( 'found item-specific layout #testLayout#', subsystem, section, item ); arrayAppend( request._fw1.layouts, testLayout ); @@ -1359,7 +1450,7 @@ component { internalFrameworkTrace( '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' ); + section & '/' & item, 'view' ); if ( cachedFileExists( request._fw1.view ) ) { internalFrameworkTrace( 'found view #request._fw1.view#', subsystem, section, item ); } else { @@ -1409,7 +1500,7 @@ component { if ( structKeyExists( cfc, method ) ) { try { internalFrameworkTrace( 'calling #lifecycle# controller', tuple.subsystem, tuple.section, method ); - evaluate( 'cfc.#method#( rc = request.context )' ); + evaluate( 'cfc.#method#( rc = request.context, headers = request._fw1.headers )' ); } catch ( any e ) { setCfcMethodFailureInfo( cfc, method ); rethrow; @@ -1417,7 +1508,7 @@ component { } else if ( structKeyExists( cfc, 'onMissingMethod' ) ) { try { internalFrameworkTrace( 'calling #lifecycle# controller (via onMissingMethod)', tuple.subsystem, tuple.section, method ); - evaluate( 'cfc.#method#( rc = request.context, method = lifecycle )' ); + evaluate( 'cfc.#method#( rc = request.context, method = lifecycle, headers = request._fw1.headers )' ); } catch ( any e ) { setCfcMethodFailureInfo( cfc, method ); rethrow; @@ -1491,7 +1582,7 @@ component { for ( var i = 1; i <= n; ++i ) { var property = md.properties[ i ]; if ( implicitSetters || - structKeyExists( property, 'setter' ) && isBoolean( property.setter ) && property.setter ) { + structKeyExists( property, 'setter' ) && isBoolean( property.setter ) && property.setter ) { setters[ property.name ] = 'implicit'; } } @@ -1532,8 +1623,8 @@ component { writeOutput( '
' ); writeOutput( '
Framework Lifecycle Trace
' ); var table = '' & - '' & - ''; + '' & + ''; writeOutput( table ); var colors = [ '##ccd4dd', '##ccddcc' ]; var row = 0; @@ -1676,8 +1767,8 @@ component { } private struct function getFw1App() { - if ( structKeyExists( variables, "fw1App" ) ) { - return variables.fw1App; + if ( structKeyExists( request._fw1, 'theApp' ) ) { + return request._fw1.theApp; } else { return application[variables.framework.applicationKey]; } @@ -1764,10 +1855,6 @@ component { if ( structKeyExists( rc, '$' ) ) { $ = rc.$; } - if ( !structKeyExists( request._fw1, 'controllerExecutionComplete' ) ) { - throw( 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" { include '#layoutPath#'; @@ -1792,7 +1879,8 @@ component { private boolean function isFrameworkInitialized() { return structKeyExists( variables, 'framework' ) && - structKeyExists( application, variables.framework.applicationKey ); + ( structKeyExists( request._fw1, 'theApp' ) || + structKeyExists( application, variables.framework.applicationKey ) ); } private boolean function isSubsystemInitialized( string subsystem ) { @@ -1854,7 +1942,7 @@ component { case 'view': folder = variables.viewFolder; break; - // else leave it alone? + // else leave it alone? } var pathInfo = { }; var subsystem = getSubsystem( getSubsystemSectionAndItem( path ) ); @@ -1867,11 +1955,11 @@ component { pathInfo.base = pathInfo.base & getSubsystemDirPrefix( subsystem ); } var defaultPath = pathInfo.base & folder & 's/' & pathInfo.path & '.cfm'; - if ( !cachedFileExists( expandPath( defaultPath ) ) ) + if ( !cachedFileExists( defaultPath ) ) defaultPath = pathInfo.base & folder & 's/' & pathInfo.path & '.lucee'; - if ( !cachedFileExists( expandPath( defaultPath ) ) ) + if ( !cachedFileExists( defaultPath ) ) defaultPath = pathInfo.base & folder & 's/' & pathInfo.path & '.lc'; - if ( !cachedFileExists( expandPath( defaultPath ) ) ) + if ( !cachedFileExists( defaultPath ) ) // can't find it so assume .cfm default value defaultPath = pathInfo.base & folder & 's/' & pathInfo.path & '.cfm'; return customizeViewOrLayoutPath( pathInfo, type, defaultPath ); @@ -1895,8 +1983,14 @@ component { if ( routeLen ) { if ( left( routeRegEx.pattern, 1 ) == '$' ) { // check HTTP method - routeRegEx.method = listFirst( routeRegEx.pattern, '*/^' ); - var methodLen = len( routeRegEx.method ); + var methodLen = 0; + if ( routeLen >= 2 && left( routeRegEx.pattern, 2 ) == '$*' ) { + // accept all methods so don't set method but... + methodLen = 2; // ...consume 2 characters + } else { + routeRegEx.method = listFirst( routeRegEx.pattern, '*/^' ); + methodLen = len( routeRegEx.method ); + } if ( routeLen == methodLen ) { routeRegEx.pattern = '*'; } else { @@ -1944,11 +2038,25 @@ component { var routeMatch = { matched = false }; structAppend( routeMatch, regExCache[ cacheKey ] ); if ( !len( path ) || right( path, 1) != '/' ) path &= '/'; - var matched = len( routeMatch.method ) ? ( '$' & httpMethod == routeMatch.method ) : true; - if ( matched && routeRegexFind( routeMatch.pattern, path ) ) { - routeMatch.matched = true; - routeMatch.route = route; - routeMatch.path = path; + if ( routeRegexFind( routeMatch.pattern, path ) ) { + if ( len( routeMatch.method ) > 1 ) { + if ( '$' & httpMethod == routeMatch.method ) { + routeMatch.matched = true; + } else if ( variables.framework.preflightOptions ) { + // it matched apart from the method so record this + request._fw1.routeMethodsMatched[ right( routeMatch.method, len( routeMatch.method ) - 1 ) ] = true; + } + } else if ( variables.framework.preflightOptions && httpMethod == "OPTIONS" ) { + // it would have matched but we should special case OPTIONS + request._fw1.routeMethodsMatched.get = true; + request._fw1.routeMethodsMatched.post = true; + } else { + routeMatch.matched = true; + } + if ( routeMatch.matched ) { + routeMatch.route = route; + routeMatch.path = path; + } } return routeMatch; } @@ -1963,7 +2071,7 @@ component { private array function getResourceRoutes( any resourcesToRoute, string subsystem = '', string pathRoot = '', string targetAppend = '' ) { var resourceCache = isFrameworkInitialized() ? getFw1App().cache.routes.resources : { }; - var cacheKey = hash( serializeJSON( resourcesToRoute ) ); + var cacheKey = hash( serializeJSON( { rtr = resourcesToRoute, ss = subsystem, pr = pathRoot, ta = targetAppend } ) ); if ( !structKeyExists( resourceCache, cacheKey ) ) { // get passed in resourcesToRoute (string,array,struct) to match following struct var resources = { resources = [ ], subsystem = subsystem, pathRoot = pathRoot, methods = [ ], nested = [ ] }; @@ -2018,63 +2126,106 @@ component { return resourceCache[ cacheKey ]; } - private string function renderDataWithContentType() { - var out = ''; - var contentType = ''; - var type = request._fw1.renderData.type; - var data = request._fw1.renderData.data; + private any function read_json( string json ) { + return deserializeJSON( json ); + } + + private struct function render_json( struct renderData ) { + return { + contentType = 'application/json; charset=utf-8', + output = serializeJSON( renderData.data ) + }; + } + + private struct function render_jsonp( struct renderData ) { + if ( !structKeyExists( renderData, 'jsonpCallback' ) || !len( renderData.jsonpCallback ) ){ + throw( type = 'FW1.jsonpCallbackRequired', + message = 'Callback was not defined', + detail = 'renderData() called with jsonp type requires a jsonpCallback' ); + } + return { + contentType = 'application/javascript; charset=utf-8', + output = renderData.jsonpCallback & "(" & serializeJSON( renderData.data ) & ");" + }; + } + + private struct function render_rawjson( struct renderData ) { + return { + contentType = 'application/json; charset=utf-8', + output = renderData.data + }; + } + + private struct function render_html( struct renderData ) { + structDelete( request._fw1, 'renderData' ); + return { + contentType = 'text/html; charset=utf-8', + output = renderData.data + }; + } + + private struct function render_xml( struct renderData ) { + var output = ''; + if ( isXML( renderData.data ) ) { + if ( isSimpleValue( renderData.data ) ) { + // XML as string already + output = renderData.data; + } else { + // XML object + output = toString( renderData.data ); + } + } else { + throw( type = 'FW1.UnsupportXMLRender', + message = 'Data is not XML', + detail = 'renderData() called with XML type but unrecognized data format' ); + } + return { + contentType = 'text/xml; charset=utf-8', + output = output + }; + } + + private struct function render_text( struct renderData ) { + return { + contentType = 'text/plain; charset=utf-8', + output = renderData.data + }; + } + + private struct function renderDataWithContentType() { + var out = { }; + var renderType = request._fw1.renderData.type; var statusCode = request._fw1.renderData.statusCode; - switch ( type ) { - case 'json': - contentType = 'application/json; charset=utf-8'; - out = serializeJSON( data ); - break; - case 'jsonp': - contentType = 'application/javascript; charset=utf-8'; - if ( !len(request._fw1.renderData.jsonpCallback) ){ - throw( type = 'FW1.jsonpCallbackRequired', - message = 'Callback was not defined', - detail = 'renderData() called with jsonp type requires a jsonpCallback' ); - } - out = request._fw1.renderData.jsonpCallback & "(" & serializeJSON( data ) & ");"; - break; - case 'rawjson': - contentType = 'application/json; charset=utf-8'; - out = data; - break; - case 'html': - contentType = 'text/html; charset=utf-8'; - out = data; - structDelete( request._fw1, 'renderData' ); - break; - case 'xml': - contentType = 'text/xml; charset=utf-8'; - if ( isXML( data ) ) { - if ( isSimpleValue( data ) ) { - // XML as string already - out = data; - } else { - // XML object - out = toString( data ); - } + var statusText = request._fw1.renderData.statusText; + var headers = structKeyExists( request._fw1.renderData, 'headers' ) ? + request._fw1.renderData.headers : [ ]; + if ( isSimpleValue( renderType ) ) { + var fn_type = 'render_' & renderType; + if ( structKeyExists( variables, fn_type ) ) { + renderType = variables[ fn_type ]; + // evaluate with no FW/1 context! + out = renderType( request._fw1.renderData ); } else { - throw( type = 'FW1.UnsupportXMLRender', - message = 'Data is not XML', - detail = 'renderData() called with XML type but unrecognized data format' ); + throw( type = 'FW1.UnsupportedRenderType', + message = 'Only HTML, JSON, JSONP, RAWJSON, XML, and TEXT are supported', + detail = 'renderData() called with unknown type: ' & renderType ); } - break; - case 'text': - contentType = 'text/plain; charset=utf-8'; - out = data; - break; - default: - throw( type = 'FW1.UnsupportedRenderType', - message = 'Only HTML, JSON, JSONP, RAWJSON, XML, and TEXT are supported', - detail = 'renderData() called with unknown type: ' & type ); - break; + } else { + // assume it is a function + out = renderType( request._fw1.renderData ); + } + var resp = getPageContext().getResponse(); + for ( var h in headers ) { + resp.setHeader( h.name, h.value ); + } + // in theory, we should use sendError() instead of setStatus() but some + // Servlet containers interpret that to mean "Send my error page" instead + // of just sending the response you actually want! + if ( len( statusText ) ) { + resp.setStatus( statusCode, statusText ); + } else { + resp.setStatus( statusCode ); } - getPageContext().getResponse().setStatus( statusCode ); - getPageContext().getResponse().setContentType( contentType ); return out; } @@ -2121,10 +2272,10 @@ component { structAppend( request.context, session[ preserveKeySessionKey ], false ); if ( variables.framework.maxNumContextsPreserved == 1 ) { /* - When multiple contexts are preserved, the oldest context is purged - within getNextPreserveKeyAndPurgeOld once the maximum is reached. - This allows for a browser refresh after the redirect to still receive - the same context. + When multiple contexts are preserved, the oldest context is purged + within getNextPreserveKeyAndPurgeOld once the maximum is reached. + This allows for a browser refresh after the redirect to still receive + the same context. */ structDelete( session, preserveKeySessionKey ); } @@ -2190,7 +2341,7 @@ component { private void function setupApplicationWrapper() { if ( structKeyExists( request._fw1, "appWrapped" ) ) return; request._fw1.appWrapped = true; - variables.fw1App = { + request._fw1.theApp = { cache = { lastReload = now(), fileExists = { }, @@ -2242,9 +2393,9 @@ component { // this will recreate the main bean factory on a reload: internalFrameworkTrace( 'setupApplication() called' ); setupApplication(); - application[variables.framework.applicationKey] = variables.fw1App; + application[variables.framework.applicationKey] = request._fw1.theApp; - } + } private void function setupFrameworkDefaults() { if ( structKeyExists( variables, "_fw1_defaults_initialized" ) ) return; @@ -2366,6 +2517,9 @@ component { if ( !structKeyExists( variables.framework, 'routes' ) ) { variables.framework.routes = [ ]; } + if ( !structKeyExists( variables.framework, 'perResourceError' ) ) { + variables.framework.perResourceError = true; + } if ( !structKeyExists( variables.framework, 'resourceRouteTemplates' ) ) { variables.framework.resourceRouteTemplates = [ { method = 'default', httpMethods = [ '$GET' ] }, @@ -2375,6 +2529,9 @@ component { { method = 'update', httpMethods = [ '$PUT','$PATCH' ], includeId = true }, { method = 'destroy', httpMethods = [ '$DELETE' ], includeId = true } ]; + if ( variables.framework.perResourceError ) { + arrayAppend( variables.framework.resourceRouteTemplates, { method = 'error', httpMethods = [ '$*' ] } ); + } } if ( !structKeyExists( variables.framework, 'routesCaseSensitive' ) ) { variables.framework.routesCaseSensitive = true; @@ -2416,6 +2573,7 @@ component { throw( type = "FW1.IllegalConfiguration", message = "ViewsFolder must be a plural word (ends in 's')." ); } + variables.viewFolder = left( variables.framework.viewsFolder, len( variables.framework.viewsFolder ) - 1 ); if ( !structKeyExists( variables.framework, 'diOverrideAllowed' ) ) { variables.framework.diOverrideAllowed = false; } @@ -2431,24 +2589,45 @@ component { if ( !structKeyExists( variables.framework, 'diComponent' ) ) { var diComponent = 'framework.ioc'; switch ( variables.framework.diEngine ) { - case 'aop1': - diComponent = 'framework.aop'; - break; - case 'wirebox': - diComponent = 'framework.WireBoxAdapter'; - break; - case 'custom': - throw( type="FW1.IllegalConfiguration", - message="If you specify diEngine='custom' you must specify a component path for diComponent." ); - break; - default: - // assume DI/1 - break; + case 'aop1': + diComponent = 'framework.aop'; + break; + case 'wirebox': + diComponent = 'framework.WireBoxAdapter'; + break; + case 'custom': + throw( type="FW1.IllegalConfiguration", + message="If you specify diEngine='custom' you must specify a component path for diComponent." ); + break; + default: + // assume DI/1 + break; } variables.framework.diComponent = diComponent; } - variables.viewFolder = left( variables.framework.viewsFolder, len( variables.framework.viewsFolder ) - 1 ); + if ( structKeyExists( variables.framework, 'enableJSONPOST' ) ) { + throw( type="FW1.IllegalConfiguration", + message="The enableJSONPOST setting has been renamed to decodeRequestBody." ); + } + if ( !structKeyExists( variables.framework, 'decodeRequestBody' ) ) { + variables.framework.decodeRequestBody = false; + } + if ( !structKeyExists( variables.framework, 'preflightOptions' ) ) { + variables.framework.preflightOptions = false; + } + if ( !structKeyExists( variables.framework, 'optionsAccessControl' ) ) { + variables.framework.optionsAccessControl = { }; + } setupEnvironment( env ); + if ( variables.framework.preflightOptions ) { + var defaultAccessControl = { + origin = "*", + headers = "Accept,Authorization,Content-Type", + credentials = true, + maxAge = 1728000 + }; + structAppend( variables.framework.optionsAccessControl, defaultAccessControl, false ); + } request._fw1.doTrace = variables.framework.trace; // add this as a fingerprint so autowire can detect FW/1 CFC: this.__fw1_version = variables.framework.version; @@ -2531,19 +2710,32 @@ component { // pathInfo is bogus so ignore it: pathInfo = ''; } + request._fw1.currentRoute = ''; var routes = getRoutes(); if ( arrayLen( routes ) ) { internalFrameworkTrace( 'processRoutes() called' ); var routeMatch = processRoutes( pathInfo, routes ); if ( routeMatch.matched ) { internalFrameworkTrace( 'route matched - #routeMatch.route# - #pathInfo#' ); - pathInfo = rereplace( routeMatch.path, routeMatch.pattern, routeMatch.target ); + var routeTail = ''; + if ( variables.framework.routesCaseSensitive ) { + pathInfo = rereplace( routeMatch.path, routeMatch.pattern, routeMatch.target ); + routeTail = rereplace( routeMatch.path, routeMatch.pattern, '' ); + } else { + pathInfo = rereplacenocase( routeMatch.path, routeMatch.pattern, routeMatch.target ); + routeTail = rereplacenocase( routeMatch.path, routeMatch.pattern, '' ); + } + request._fw1.currentRoute = left( routeMatch.path, len( routeMatch.path ) - len( routeTail ) ); if ( routeMatch.redirect ) { location( pathInfo, false, routeMatch.statusCode ); } else { request._fw1.route = routeMatch.route; } } + } else if ( variables.framework.preflightOptions && request._fw1.cgiRequestMethod == "OPTIONS" ) { + // non-route matching but we have OPTIONS support enabled + request._fw1.routeMethodsMatched.get = true; + request._fw1.routeMethodsMatched.post = true; } try { // we use .split() to handle empty items in pathInfo - we fallback to listToArray() on @@ -2561,6 +2753,13 @@ component { pathInfo = listToArray( pathInfo, '/' ); } var sesN = arrayLen( pathInfo ); + if ( !len( request._fw1.currentRoute ) ) { + switch ( sesN ) { + case 0 : request._fw1.currentRoute = '/'; break; + case 1 : request._fw1.currentRoute = '/' & pathInfo[1] & '/'; break; + default: request._fw1.currentRoute = '/' & pathInfo[1] & '/' & pathInfo[2] & '/'; break; + } + } if ( ( sesN > 0 || variables.framework.generateSES ) && getBaseURL() != 'useRequestURI' ) { request._fw1.generateSES = true; } @@ -2576,18 +2775,57 @@ component { } } // certain remote calls do not have URL or form scope: - if ( isDefined('URL') ) structAppend(request.context,URL); - if ( isDefined('form') ) structAppend(request.context,form); + if ( isDefined( 'URL' ) ) structAppend( request.context, URL ); + if ( isDefined( 'form' ) ) structAppend( request.context, form ); + var httpData = getHttpRequestData(); + if ( variables.framework.decodeRequestBody ) { + // thanks to Adam Tuttle and by proxy Jason Dean and Ray Camden for the + // seed of this code, inspired by Taffy's basic deserialization + // also thanks to John Whish for the URL-encoded form support + // which adds support for PUT etc + var body = httpData.content; + if ( isBinary( body ) ) body = charSetEncode( body, "utf-8" ); + if ( len( body ) ) { + switch ( listFirst( CGI.CONTENT_TYPE, ';' ) ) { + case "application/json": + case "text/json": + try { + var bodyStruct = read_json( body ); + structAppend( request.context, bodyStruct ); + } catch ( any e ) { + throw( type = "FW1.JSONPOST", + message = "Content-Type implies JSON but could not deserialize body: " & e.message ); + } + break; + case "application/x-www-form-urlencoded": + try { + var paramPairs = listToArray( body, "&" ); + for ( var pair in paramPairs ) { + var parts = listToArray( pair, "=", true ); // handle blank values + request.context[ parts[ 1 ] ] = urlDecode( parts[ 2 ] ); + } + } catch ( any e ) { + throw( type = "FW1.JSONPOST", + message = "Content-Type implies form encoded but could not deserialize body: " & e.message ); + } + break; + default: + // ignore -- either built-in (form handling) or unsupported + break; + } + } + } + request._fw1.headers = httpData.headers; // figure out the request action before restoring flash context: - if ( !structKeyExists(request.context, variables.framework.action) ) { - request.context[variables.framework.action] = getFullyQualifiedAction( variables.framework.home ); + if ( !structKeyExists( request.context, variables.framework.action ) ) { + request.context[ variables.framework.action ] = getFullyQualifiedAction( variables.framework.home ); } else { - request.context[variables.framework.action] = getFullyQualifiedAction( request.context[variables.framework.action] ); + request.context[ variables.framework.action ] = getFullyQualifiedAction( request.context[ variables.framework.action ] ); } if ( variables.framework.noLowerCase ) { - request.action = validateAction( request.context[variables.framework.action] ); + request.action = validateAction( request.context[ variables.framework.action ] ); } else { - request.action = validateAction( lCase(request.context[variables.framework.action]) ); + request.action = validateAction( lCase(request.context[ variables.framework.action ]) ); } request._fw1.requestDefaultsInitialized = true; } @@ -2599,6 +2837,7 @@ component { request.subsystembase = request.base & getSubsystemDirPrefix( request.subsystem ); request.section = getSection( request.action ); request.item = getItem( request.action ); + request._fw1.theFramework = this; // for use in the facade (only!) if ( runSetup ) { controller( variables.magicApplicationSubsystem & variables.framework.subsystemDelimiter & @@ -2651,8 +2890,13 @@ component { } if ( len( sublocations ) ) { var diComponent = structKeyExists( subsystemConfig, 'diComponent' ) ? subsystemConfig : variables.framework.diComponent; - var cfg = structKeyExists( subsystemConfig, 'diConfig' ) ? - subsystemConfig.diConfig : structCopy( variables.framework.diConfig ); + var cfg = { }; + if ( structKeyExists( subsystemConfig, 'diConfig' ) ) { + cfg = subsystemConfig.diConfig; + } else { + cfg = structCopy( variables.framework.diConfig ); + structDelete( cfg, 'loadListener' ); + } cfg.noClojure = true; var ioc = new "#diComponent#"( subLocations, cfg ); ioc.setParent( getDefaultBeanFactory() ); @@ -2671,7 +2915,7 @@ component { // 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 ) { throw( type='FW1.actionContainsSlash', message="Found a slash in the action: '#action#'.", - detail='Actions are not allowed to embed sub-directory paths.'); + detail='Actions are not allowed to embed sub-directory paths.'); } return action; } @@ -2682,7 +2926,7 @@ component { // the exception we actually want to throw! param name="request.missingView" default=""; throw( type='FW1.viewNotFound', message="Unable to find a view for '#request.action#' action.", - detail="'#request.missingView#' does not exist." ); + detail="'#request.missingView#' does not exist." ); } } diff --git a/introduction/layouts/default.cfm b/introduction/layouts/default.cfm index e9da1cee..0283b989 100644 --- a/introduction/layouts/default.cfm +++ b/introduction/layouts/default.cfm @@ -11,7 +11,7 @@ #body# diff --git a/introduction/views/main/default.cfm b/introduction/views/main/default.cfm index 093ac542..9cc61209 100644 --- a/introduction/views/main/default.cfm +++ b/introduction/views/main/default.cfm @@ -1,9 +1,9 @@

Welcome to Framework One!

#view('about/default')#

Documentation

-

The documentation for FW/1 can be found on its github wiki page. - The latest news about FW/1 can always be found on its RIAForge project page - which also has links to the mailing list and - blog.

+

Read the documentation for FW/1. + The latest news can always be found on the FW/1 blog + which also has links to the FW/1 mailing list + and other avenues for community support.

Examples

#view('main/examples')# diff --git a/tests/AddBeanTest.cfc b/tests/AddBeanTest.cfc index 87aee3ed..9937d57c 100644 --- a/tests/AddBeanTest.cfc +++ b/tests/AddBeanTest.cfc @@ -3,7 +3,7 @@ component extends="mxunit.framework.TestCase" { function setup() { variables.added = new framework.ioc( "" ) - .addBean( "known", 42 ); + .declare( "known" ).asValue( 42 ).done(); } function shouldHaveKnownValue() { diff --git a/tests/BeanInfoTest.cfc b/tests/BeanInfoTest.cfc index 726a20e7..68b0b7c3 100644 --- a/tests/BeanInfoTest.cfc +++ b/tests/BeanInfoTest.cfc @@ -29,7 +29,7 @@ component extends="mxunit.framework.TestCase" { function shouldBeFlat() { var parent = new framework.ioc( "" ); - parent.addBean( "father", "figure" ); + parent.declare( "father" ).asValue( "figure" ); variables.factory.setParent( parent ); var info = variables.factory.getBeanInfo( flatten = true ); assertFalse( structKeyExists( info, "parent" ) ); @@ -38,8 +38,9 @@ component extends="mxunit.framework.TestCase" { } function shouldMatchRegex() { - variables.factory.addBean( "father", "figure" ); - variables.factory.addBean( "mother", "figure" ); + variables.factory + .declare( "father" ).asValue( "figure" ).done() + .addBean( "mother", "figure" ); var info = variables.factory.getBeanInfo( regex = "her$" ); assertEquals( 2, structCount( info.beaninfo ) ); assertTrue( structKeyExists( info.beaninfo, "father" ) ); diff --git a/tests/DeclareBeanTest.cfc b/tests/DeclareBeanTest.cfc index 5359587e..0f0df62c 100644 --- a/tests/DeclareBeanTest.cfc +++ b/tests/DeclareBeanTest.cfc @@ -1,7 +1,8 @@ component extends="mxunit.framework.TestCase" { function shouldDeclareSingleton() { - var bf = new framework.ioc( "" ).declareBean( "foo", "tests.extrabeans.sheep.item" ); + var bf = new framework.ioc( "" ) + .declare( "foo" ).instanceOf( "tests.extrabeans.sheep.item" ).done(); structDelete( application, "itemCount" ); var item1 = bf.getBean( "foo" ); assertEquals( 1, application.itemCount ); @@ -11,7 +12,11 @@ component extends="mxunit.framework.TestCase" { } function shouldDeclareTransient() { - var bf = new framework.ioc( "" ).declareBean( "foo", "tests.extrabeans.sheep.item", false ); + var bf = new framework.ioc( "" ) + .declare( "foo" ) + .instanceOf( "tests.extrabeans.sheep.item" ) + .asTransient() + .done(); structDelete( application, "itemCount" ); var item1 = bf.getBean( "foo" ); assertEquals( 1, application.itemCount ); @@ -21,7 +26,11 @@ component extends="mxunit.framework.TestCase" { } function shouldDeclareSingletonWithOverride() { - var bf = new framework.ioc( "" ).declareBean( "foo", "tests.extrabeans.sheep.item", true, { start = 100 } ); + var bf = new framework.ioc( "" ) + .declare( "foo" ) + .instanceOf( "tests.extrabeans.sheep.item" ) + .withOverrides( { start = 100 } ) + .done(); structDelete( application, "itemCount" ); var item1 = bf.getBean( "foo" ); assertEquals( 101, application.itemCount ); @@ -31,7 +40,12 @@ component extends="mxunit.framework.TestCase" { } function shouldDeclareTransientWithOverride() { - var bf = new framework.ioc( "" ).declareBean( "foo", "tests.extrabeans.sheep.item", false, { start = 100 } ); + var bf = new framework.ioc( "" ) + .declare( "foo" ) + .instanceOf( "tests.extrabeans.sheep.item" ) + .asTransient() + .withOverrides( { start = 100 } ) + .done(); structDelete( application, "itemCount" ); var item1 = bf.getBean( "foo" ); assertEquals( 101, application.itemCount ); @@ -41,18 +55,35 @@ component extends="mxunit.framework.TestCase" { } function shouldDeclareAndAdd() { - var bf = new framework.ioc( "" ).declareBean( "foo", "tests.declared.things.myconfig" ).addBean( "name", "test" ).addBean( "config", "some" ); + var bf = new framework.ioc( "", { omitTypedProperties = false } ) + .declare( "foo" ).instanceOf( "tests.declared.things.myconfig" ).done() + .declare( "name" ).asValue( "test" ).done() + .declare( "config" ).asValue( "some" ).done(); var item = bf.getBean( "foo" ); assertEquals( "test", item.getName() ); assertEquals( "some", item.getConfig() ); } function shouldDeclareWithOverride() { - var bf = new framework.ioc( "" ).declareBean( "foo", "tests.declared.things.myconfig", true, { name = "test", config = "some" } ) + var bf = new framework.ioc( "", { omitTypedProperties = false } ) + .declare( "foo" ) + .instanceOf( "tests.declared.things.myconfig" ) + .withOverrides( { name = "test", config = "some" } ).done() .addBean( "name", "not-test" ).addBean( "config", "config" ); var item = bf.getBean( "foo" ); assertEquals( "test", item.getName() ); assertEquals( "some", item.getConfig() ); } + function shouldDeclareInteractWithDefault() { + var bf = new framework.ioc( "", { omitDefaultedProperties = false } ).declareBean( "foo", "tests.declared.things.myconfig" ) + .addBean( "dftname", "injected" ); + var item = bf.getBean( "foo" ); + assertEquals( "injected", item.getDftName() ); + var bf = new framework.ioc( "", { omitDefaultedProperties = true } ).declareBean( "foo", "tests.declared.things.myconfig" ) + .addBean( "dftname", "injected" ); + var item = bf.getBean( "foo" ); + assertEquals( "default", item.getDftName() ); + } + } diff --git a/tests/FactoryBeanTest.cfc b/tests/FactoryBeanTest.cfc index 945ee3bf..162ec4d6 100644 --- a/tests/FactoryBeanTest.cfc +++ b/tests/FactoryBeanTest.cfc @@ -2,7 +2,15 @@ component extends="mxunit.framework.TestCase" { function shouldSupportBasicFactoryMethod() { var bf = new framework.ioc( "/tests/model" ); - bf.factoryBean( "a", "factory", "makeMeAnA" ); + bf.declare( "a" ).fromFactory( "factory", "makeMeAnA" ); + assertEquals( "I am an A", bf.getBean( "a" ) ); + } + + function shouldSupportFactoryFunction() { + var bf = new framework.ioc( "/tests/model" ); + bf.declare( "a" ).fromFactory( function() { + return "I am an A"; + } ); assertEquals( "I am an A", bf.getBean( "a" ) ); } @@ -15,14 +23,18 @@ component extends="mxunit.framework.TestCase" { function shouldSupportFactoryMethodWithBeanArg() { var bf = new framework.ioc( "/tests/model" ); - bf.factoryBean( "a", "factory", "makeAWithFava", [ "favaBean" ] ); + bf.declare( "a" ) + .fromFactory( "factory", "makeAWithFava" ) + .withArguments( [ "favaBean" ] ); assertEquals( "I am a fava bean", bf.getBean( "a" ) ); } function shouldSupportFactoryMethodWithLocalArg() { var bf = new framework.ioc( "/tests/model" ); - bf.factoryBean( "a", "factory", "makeAWithFava", [ "favaBean" ], - { favaBean = { stamp = "different" } } ); + bf.declare( "a" ) + .fromFactory( "factory", "makeAWithFava" ) + .withArguments( [ "favaBean" ] ) + .withOverrides( { favaBean = { stamp = "different" } } ); assertEquals( "I am a different bean", bf.getBean( "a" ) ); } diff --git a/tests/TransientInjectionTest.cfc b/tests/TransientInjectionTest.cfc new file mode 100644 index 00000000..9a8d70c0 --- /dev/null +++ b/tests/TransientInjectionTest.cfc @@ -0,0 +1,22 @@ +component extends="mxunit.framework.TestCase" { + + function shouldReturnWiredTransient() { + // issue #420 + var bf = new framework.ioc( "" ); + bf.declareBean("transient", "tests.issue420.transient", false); + bf.declareBean("singleton", "tests.issue420.singleton", true); + + assertTrue( bf.containsBean( "transient" ) ); + assertFalse( bf.isSingleton( "transient" ) ); + + var singleton = bf.getBean( "transient" ).getSingleton(); + assertTrue( isValid( "component", singleton ), "should return the singleton instance on the 1st call" ); + assertTrue( isValid( "component", singleton.getBeanFactory() ), "should return ioc instance on the 1st call" ); + + // call again to check subsequent calls return wired transient + singleton = bf.getBean( "transient" ).getSingleton(); + assertTrue( isValid( "component", singleton ), "should return the singleton instance on the 2nd call" ); + assertTrue( isValid( "component", singleton.getBeanFactory() ), "should return ioc instance on the 2nd call" ); + } + +} diff --git a/tests/TransientTest.cfc b/tests/TransientTest.cfc index 3aa12e19..f6613f50 100644 --- a/tests/TransientTest.cfc +++ b/tests/TransientTest.cfc @@ -21,4 +21,42 @@ component extends="mxunit.framework.TestCase" { assertTrue( isObject( product ) ); } + function shouldInitializeWithBeans() { + variables.factory = new framework.ioc( "/tests/model, /tests/extrabeans", + { transients = [ "fish" ], singulars = { sheep = "bean" } } ) + .addBean( "one", 1 ).addBean( "two", "two" ); + var i = variables.factory.getBean( "item" ); + var n1 = application.itemCount; + var c = variables.factory.getBean( "construct" ); + assertEquals( 1, c.getOne() ); + assertEquals( "two", c.two ); + assertEquals( n1 + 1, application.itemCount ); + } + + function shouldInitializeWithConstructorArgs() { + variables.factory = new framework.ioc( "/tests/model, /tests/extrabeans", + { transients = [ "fish" ], singulars = { sheep = "bean" } } ) + .addBean( "one", 1 ).addBean( "two", "two" ); + var i = variables.factory.getBean( "item" ); + var n1 = application.itemCount; + var c = variables.factory.getBean( "construct", { one : "one", two : 2, item : "no-op" } ); + assertEquals( "one", c.getOne() ); + assertEquals( 2, c.two ); + assertEquals( n1, application.itemCount ); + } + + function shouldInitializeWithOnlyConstructorArgs() { + variables.factory = new framework.ioc( "/tests/extrabeans", + { singulars = { sheep = "bean" } } ); + var i = variables.factory.getBean( "item" ); + var n1 = application.itemCount; + var c1 = variables.factory.getBean( "construct", { one : "one", two : 2, item : "no-op" } ); + assertTrue( isNull( c1.getOne() ) ); + assertEquals( 2, c1.two ); + assertEquals( n1, application.itemCount ); + var c2 = variables.factory.getBean( "construct", { one : 1, two : "two", item : "something" } ); + assertTrue( isNull( c2.getOne() ) ); + assertEquals( "two", c2.two ); + } + } diff --git a/tests/declared/things/myconfig.cfc b/tests/declared/things/myconfig.cfc index 22341ebb..2e562951 100644 --- a/tests/declared/things/myconfig.cfc +++ b/tests/declared/things/myconfig.cfc @@ -1,6 +1,9 @@ component accessors=true { property string name; + property name="dftname" default="default"; + // not legal syntax: property dftname="default"; + // legal syntax, doesn't create setter: property string dftname="default"; function init( string data = "none" ) { setConfig( data ); diff --git a/tests/defaultPropertyTest.cfc b/tests/defaultPropertyTest.cfc new file mode 100644 index 00000000..f05e0369 --- /dev/null +++ b/tests/defaultPropertyTest.cfc @@ -0,0 +1,25 @@ +component extends=mxunit.framework.TestCase { + + function setup() { + variables.bf = new framework.ioc( "" ) + .declare( "default" ).instanceOf( "tests.extrabeans.sheep.default" ) + .asTransient() + .done(); + } + + function shouldHaveDefaultValue() { + var data = { + viaNew : new tests.extrabeans.sheep.default(), + viaDI1 : variables.bf.getBean( "default" ) + }; + assertTrue( isNull( data.viaNew.getSimple() ) ); + assertTrue( isNull( data.viaNew.getTyped() ) ); + assertEquals( "Default Value", data.viaNew.getDefaulted() ); + assertEquals( "Default Type Value", data.viaNew.getDefaultedType() ); + assertTrue( isNull( data.viaDI1.getSimple() ) ); + assertTrue( isNull( data.viaDI1.getTyped() ) ); + assertEquals( "Default Value", data.viaDI1.getDefaulted() ); + assertEquals( "Default Type Value", data.viaDI1.getDefaultedType() ); + } + +} diff --git a/tests/extrabeans/sheep/construct.cfc b/tests/extrabeans/sheep/construct.cfc new file mode 100644 index 00000000..0a09ebcd --- /dev/null +++ b/tests/extrabeans/sheep/construct.cfc @@ -0,0 +1,8 @@ +component accessors=true { + property one; + function init(two,item) { + this.two = two; + this.item = item; + return this; + } +} diff --git a/tests/extrabeans/sheep/default.cfc b/tests/extrabeans/sheep/default.cfc new file mode 100644 index 00000000..92e40e02 --- /dev/null +++ b/tests/extrabeans/sheep/default.cfc @@ -0,0 +1,6 @@ +component accessors=true { + property simple; + property name="typed" type="string"; + property name="defaulted" default="Default Value"; + property name="defaultedType" type="string" default="Default Type Value"; +} diff --git a/tests/frameworkEnvTest.cfc b/tests/frameworkEnvTest.cfc index 6bb90e5e..10059d6a 100644 --- a/tests/frameworkEnvTest.cfc +++ b/tests/frameworkEnvTest.cfc @@ -17,6 +17,10 @@ component extends="tests.InjectableTest" { }; } + public void function testRequestMethod() { + assertEquals( CGI.REQUEST_METHOD, variables.fw.getCGIRequestMethod() ); + } + public void function testGetEnvironmentIsCalled() { variables.fw.getEnvironment = recordCalls; variables.fwvars.getEnvironment = recordCalls; diff --git a/tests/frameworkFacadeTest.cfc b/tests/frameworkFacadeTest.cfc new file mode 100644 index 00000000..954a9828 --- /dev/null +++ b/tests/frameworkFacadeTest.cfc @@ -0,0 +1,25 @@ +component extends="mxunit.framework.TestCase" { + + function setup() { + structDelete( request, "_fw1" ); // clean up the request + } + + function testFacadeOnNonFW1Request() { + try { + var facade = new framework.facade(); + fail( "facade creation did not fail" ); + } catch ( FW1.FacadeException e ) { + assertEquals( "Unable to locate FW/1 for this request", e.message ); + } catch ( any e ) { + fail( "caught unexpected exception: " & e.message ); + } + } + + function testFacadeWithFW1() { + var fw = new framework.one(); + fw.onRequestStart( "" ); + var facade = new framework.facade(); + assertTrue( structKeyExists( facade, "getBeanFactory" ), "Constructed facade does not look like FW/1" ); + } + +} diff --git a/tests/frameworkProcessRoutesTest.cfc b/tests/frameworkProcessRoutesTest.cfc index c470d995..71fef98d 100755 --- a/tests/frameworkProcessRoutesTest.cfc +++ b/tests/frameworkProcessRoutesTest.cfc @@ -17,6 +17,7 @@ component extends="tests.InjectableTest" { { 'hint' = 'Resource Routes', '$RESOURCES' = 'dogs' } ]; variables.fwVars.framework.routesCaseSensitive = true; + variables.fwVars.framework.preflightOptions = true; } public void function testProcessRoutes() { @@ -41,7 +42,7 @@ component extends="tests.InjectableTest" { assertEquals( '/dogs/update/id/42/', rereplace( routeMatch.path, routeMatch.pattern, routeMatch.target ) ); } - + public void function testProcessRoutesExplicit() { request._fw1.cgiRequestMethod = 'FOO'; @@ -62,7 +63,35 @@ component extends="tests.InjectableTest" { assertEquals( '/dogs/update/id/42/', rereplace( routeMatch.path, routeMatch.pattern, routeMatch.target ) ); } - + + public void function testProcessRoutesOptions() { + + request._fw1.cgiRequestMethod = 'OPTIONS'; + + request._fw1.routeMethodsMatched = { }; + var routeMatch = variables.fw.processRoutes( '/no/match', variables.fw.getRoutes() ); + assertFalse( routeMatch.matched ); + assertEquals( { }, request._fw1.routeMethodsMatched ); + + request._fw1.routeMethodsMatched = { }; + routeMatch = variables.fw.processRoutes( '/old/path/foo', variables.fw.getRoutes() ); + assertFalse( routeMatch.matched ); + assertEquals( { get : true }, request._fw1.routeMethodsMatched ); + + // these show we correctly get methods back based on the ACTUAL route matched + + request._fw1.routeMethodsMatched = { }; + routeMatch = variables.fw.processRoutes( '/dogs/42', variables.fw.getRoutes() ); + assertFalse( routeMatch.matched ); + assertEquals( { get : true, delete : true, patch : true, put : true }, request._fw1.routeMethodsMatched ); + + request._fw1.routeMethodsMatched = { }; + routeMatch = variables.fw.processRoutes( '/dogs', variables.fw.getRoutes() ); + assertFalse( routeMatch.matched ); + assertEquals( { get : true, post : true }, request._fw1.routeMethodsMatched ); + + } + // PRIVATE private boolean function isFrameworkInitialized() { diff --git a/tests/frameworkRenderTest.cfc b/tests/frameworkRenderTest.cfc index ec545e69..f58b858e 100644 --- a/tests/frameworkRenderTest.cfc +++ b/tests/frameworkRenderTest.cfc @@ -79,6 +79,40 @@ component extends="mxunit.framework.TestCase" { assertTrue( output contains "framework lifecycle trace" ); } + public void function testNoTraceRenderVarBuilder() { + variables.fw.onApplicationStart(); + variables.fw.renderData( "text" ).data( "test" ); + var output = ""; + savecontent variable="output" { + variables.fw.onRequestEnd(); + } + assertFalse( output contains "framework lifecycle trace" ); + } + + public void function testTraceOutputReqBuilder() { + request.fw.onApplicationStart(); + variables.fw.renderData().type( "text" ).data( "myteststring" ); + var output = ""; + savecontent variable="output" { + request.fw.onRequest("/index.cfm"); + request.fw.onRequestEnd(); + } + assertTrue( output contains "myteststring" ); + assertFalse( output contains "framework lifecycle trace" ); + } + + public void function testTraceOutputHTMLReqBuilder() { + request.fw.onApplicationStart(); + variables.fw.renderData( data = "

myteststring

" ).type( "html" ); + var output = ""; + savecontent variable="output" { + request.fw.onRequest("/index.cfm"); + request.fw.onRequestEnd(); + } + assertTrue( output contains "myteststring" ); + assertTrue( output contains "framework lifecycle trace" ); + } + public void function testSetupTraceRenderHtml() { variables.fwExtended.onApplicationStart(); var output = ""; @@ -98,5 +132,25 @@ component extends="mxunit.framework.TestCase" { assertEquals( output, "custom trace render" ); } + public void function testRenderFunction() { + request.fw.onApplicationStart(); + variables.fw.renderData().type( function( renderData ) { + return { + contentType = "text/html; charset=utf-8", + output = "string", + writer = function( out ) { + writeOutput( "my written " & out ); + } + }; + } ).data( "myteststring" ); + var output = ""; + savecontent variable="output" { + request.fw.onRequest("/index.cfm"); + request.fw.onRequestEnd(); + } + assertTrue( output contains "my written string" ); + assertFalse( output contains "framework lifecycle trace" ); + } + } diff --git a/tests/frameworkRouteTest.cfc b/tests/frameworkRouteTest.cfc index 63847935..5791c8f6 100644 --- a/tests/frameworkRouteTest.cfc +++ b/tests/frameworkRouteTest.cfc @@ -17,6 +17,11 @@ component extends="tests.InjectableTest" { assertEquals("/test/(.*)", match.pattern); assertEquals("routed/\1", match.target); + match = variables.fw.processRouteMatch("/", "routed", "/test", "GET"); + assertTrue(match.matched); + assertEquals("/(.*)", match.pattern); + assertEquals("routed/\1", match.target); + match = variables.fw.processRouteMatch("/test2/:id", "default.main?id=:id", "/test2/5", "GET"); assertTrue(match.matched); assertEquals("/test2/([^/]*)/(.*)", match.pattern); @@ -87,6 +92,24 @@ component extends="tests.InjectableTest" { match = variables.fw.processRouteMatch("$POST^/test/:id", "default.main?id=:id", "/foo/test/5", "POST"); assertFalse(match.matched); + + match = variables.fw.processRouteMatch("$*^/test/:id", "default.main?id=:id", "/test/5", "GET"); + assertTrue(match.matched); + + match = variables.fw.processRouteMatch("$*^/test/:id", "default.main?id=:id", "/test/5", "POST"); + assertTrue(match.matched); + + match = variables.fw.processRouteMatch("$*^/test/:id", "default.main?id=:id", "/foo/test/5", "GET"); + assertFalse(match.matched); + + match = variables.fw.processRouteMatch("$*^/test/:id", "default.main?id=:id", "/foo/test/5", "POST"); + assertFalse(match.matched); + + match = variables.fw.processRouteMatch("$*", "default.error", "/foo/test/5", "GET"); + assertTrue(match.matched); + + match = variables.fw.processRouteMatch("$*", "default.error", "/foo/test/5", "POST"); + assertTrue(match.matched); } public void function testRouteMatchRedirect() diff --git a/tests/issue420/singleton.cfc b/tests/issue420/singleton.cfc new file mode 100644 index 00000000..72d3884a --- /dev/null +++ b/tests/issue420/singleton.cfc @@ -0,0 +1,8 @@ +component accessors="true" { + + property name="beanFactory"; + + function init() { + return this; + } +} diff --git a/tests/issue420/transient.cfc b/tests/issue420/transient.cfc new file mode 100644 index 00000000..3aea6dbf --- /dev/null +++ b/tests/issue420/transient.cfc @@ -0,0 +1,8 @@ +component accessors="true" { + + property name="singleton"; + + function init() { + return this; + } +} diff --git a/tests/onMissingViewLayoutTest.cfc b/tests/onMissingViewLayoutTest.cfc index 3998f95f..c9ef8cb0 100644 --- a/tests/onMissingViewLayoutTest.cfc +++ b/tests/onMissingViewLayoutTest.cfc @@ -7,6 +7,7 @@ component extends="tests.InjectableTest" { variables.fwvars.framework = { base = "/tests/omv" }; + structDelete( request, "layout" ); } public void function testSetLayout() { @@ -33,6 +34,24 @@ component extends="tests.InjectableTest" { assertEquals( "TWOTEST", trim( output ) ); } + public void function testCustomRenderer() { + var omv = function( rc ) { + request.layout = false; + return { + contentType = "text/html; charset=utf-8", + output = "custom output" + }; + }; + variables.fw.onMissingView = omv; + variables.fwvars.onMissingView = omv; + variables.fw.onRequestStart( "" ); + var output = ""; + savecontent variable="output" { + variables.fw.onRequest( "" ); + } + assertEquals( "custom output", trim( output ) ); + } + private string function selectLayoutTwo( struct rc ) { setLayout( "main.two" ); return view( "main/test" );
timedeltatypeactionmessage
timedeltatypeactionmessage