Add API Framework Revel Source Files 85/2185/1
authorTrevor Tao <trevor.tao@arm.com>
Thu, 9 Jan 2020 16:00:15 +0000 (00:00 +0800)
committerTrevor Tao <trevor.tao@arm.com>
Thu, 9 Jan 2020 16:00:15 +0000 (00:00 +0800)
Add API framework Revel source files, as the basis for api framework.

Signed-off-by: Trevor Tao <trevor.tao@arm.com>
Change-Id: I8c60569238a66538e86a2cc98a0d6473df2f8e72

125 files changed:
src/foundation/api/revel/.codebeatsettings [new file with mode: 0644]
src/foundation/api/revel/.travis.yml [new file with mode: 0644]
src/foundation/api/revel/AUTHORS [new file with mode: 0644]
src/foundation/api/revel/CHANGELOG.md [new file with mode: 0644]
src/foundation/api/revel/CONTRIBUTING.md [new file with mode: 0644]
src/foundation/api/revel/LICENSE [new file with mode: 0644]
src/foundation/api/revel/README.md [new file with mode: 0644]
src/foundation/api/revel/before_after_filter.go [new file with mode: 0644]
src/foundation/api/revel/binder.go [new file with mode: 0644]
src/foundation/api/revel/binder_test.go [new file with mode: 0644]
src/foundation/api/revel/cache/cache.go [new file with mode: 0644]
src/foundation/api/revel/cache/cache_test.go [new file with mode: 0644]
src/foundation/api/revel/cache/init.go [new file with mode: 0644]
src/foundation/api/revel/cache/inmemory.go [new file with mode: 0644]
src/foundation/api/revel/cache/inmemory_test.go [new file with mode: 0644]
src/foundation/api/revel/cache/memcached.go [new file with mode: 0644]
src/foundation/api/revel/cache/memcached_test.go [new file with mode: 0644]
src/foundation/api/revel/cache/redis.go [new file with mode: 0644]
src/foundation/api/revel/cache/redis_test.go [new file with mode: 0644]
src/foundation/api/revel/cache/serialization.go [new file with mode: 0644]
src/foundation/api/revel/cache/serialization_test.go [new file with mode: 0644]
src/foundation/api/revel/compress.go [new file with mode: 0644]
src/foundation/api/revel/compress_test.go [new file with mode: 0644]
src/foundation/api/revel/conf/mime-types.conf [new file with mode: 0644]
src/foundation/api/revel/controller.go [new file with mode: 0644]
src/foundation/api/revel/controller_type.go [new file with mode: 0644]
src/foundation/api/revel/errors.go [new file with mode: 0644]
src/foundation/api/revel/event.go [new file with mode: 0644]
src/foundation/api/revel/event_test.go [new file with mode: 0644]
src/foundation/api/revel/fakeapp_test.go [new file with mode: 0644]
src/foundation/api/revel/field.go [new file with mode: 0644]
src/foundation/api/revel/filter.go [new file with mode: 0644]
src/foundation/api/revel/filterconfig.go [new file with mode: 0644]
src/foundation/api/revel/filterconfig_test.go [new file with mode: 0644]
src/foundation/api/revel/flash.go [new file with mode: 0644]
src/foundation/api/revel/http.go [new file with mode: 0644]
src/foundation/api/revel/i18n.go [new file with mode: 0644]
src/foundation/api/revel/i18n_test.go [new file with mode: 0644]
src/foundation/api/revel/intercept.go [new file with mode: 0644]
src/foundation/api/revel/intercept_test.go [new file with mode: 0644]
src/foundation/api/revel/invoker.go [new file with mode: 0644]
src/foundation/api/revel/invoker_test.go [new file with mode: 0644]
src/foundation/api/revel/libs.go [new file with mode: 0644]
src/foundation/api/revel/libs_test.go [new file with mode: 0644]
src/foundation/api/revel/logger.go [new file with mode: 0644]
src/foundation/api/revel/logger/composite_multihandler.go [new file with mode: 0644]
src/foundation/api/revel/logger/doc.go [new file with mode: 0644]
src/foundation/api/revel/logger/handlers.go [new file with mode: 0644]
src/foundation/api/revel/logger/init.go [new file with mode: 0644]
src/foundation/api/revel/logger/init_test.go [new file with mode: 0644]
src/foundation/api/revel/logger/log_function_map.go [new file with mode: 0644]
src/foundation/api/revel/logger/logger.go [new file with mode: 0644]
src/foundation/api/revel/logger/revel_logger.go [new file with mode: 0644]
src/foundation/api/revel/logger/terminal_format.go [new file with mode: 0644]
src/foundation/api/revel/logger/utils.go [new file with mode: 0644]
src/foundation/api/revel/logger/wrap_handlers.go [new file with mode: 0644]
src/foundation/api/revel/model/revel_container.go [new file with mode: 0644]
src/foundation/api/revel/module.go [new file with mode: 0644]
src/foundation/api/revel/namespace.go [new file with mode: 0644]
src/foundation/api/revel/panic.go [new file with mode: 0644]
src/foundation/api/revel/params.go [new file with mode: 0644]
src/foundation/api/revel/params_test.go [new file with mode: 0644]
src/foundation/api/revel/results.go [new file with mode: 0644]
src/foundation/api/revel/results_test.go [new file with mode: 0644]
src/foundation/api/revel/revel.go [new file with mode: 0644]
src/foundation/api/revel/revel_hooks.go [new file with mode: 0644]
src/foundation/api/revel/revel_test.go [new file with mode: 0644]
src/foundation/api/revel/router.go [new file with mode: 0644]
src/foundation/api/revel/router_test.go [new file with mode: 0644]
src/foundation/api/revel/server-engine.go [new file with mode: 0644]
src/foundation/api/revel/server.go [new file with mode: 0644]
src/foundation/api/revel/server_adapter_go.go [new file with mode: 0644]
src/foundation/api/revel/server_test.go [new file with mode: 0644]
src/foundation/api/revel/session/init.go [new file with mode: 0644]
src/foundation/api/revel/session/session.go [new file with mode: 0644]
src/foundation/api/revel/session/session_cookie_test.go [new file with mode: 0644]
src/foundation/api/revel/session/session_test.go [new file with mode: 0644]
src/foundation/api/revel/session_adapter_cookie.go [new file with mode: 0644]
src/foundation/api/revel/session_engine.go [new file with mode: 0644]
src/foundation/api/revel/session_filter.go [new file with mode: 0644]
src/foundation/api/revel/template.go [new file with mode: 0644]
src/foundation/api/revel/template_adapter_go.go [new file with mode: 0644]
src/foundation/api/revel/template_engine.go [new file with mode: 0644]
src/foundation/api/revel/template_functions.go [new file with mode: 0644]
src/foundation/api/revel/templates/errors/403.html [new file with mode: 0644]
src/foundation/api/revel/templates/errors/403.json [new file with mode: 0644]
src/foundation/api/revel/templates/errors/403.txt [new file with mode: 0644]
src/foundation/api/revel/templates/errors/403.xml [new file with mode: 0644]
src/foundation/api/revel/templates/errors/404-dev.html [new file with mode: 0644]
src/foundation/api/revel/templates/errors/404.html [new file with mode: 0644]
src/foundation/api/revel/templates/errors/404.json [new file with mode: 0644]
src/foundation/api/revel/templates/errors/404.txt [new file with mode: 0644]
src/foundation/api/revel/templates/errors/404.xml [new file with mode: 0644]
src/foundation/api/revel/templates/errors/405.html [new file with mode: 0644]
src/foundation/api/revel/templates/errors/405.json [new file with mode: 0644]
src/foundation/api/revel/templates/errors/405.txt [new file with mode: 0644]
src/foundation/api/revel/templates/errors/405.xml [new file with mode: 0644]
src/foundation/api/revel/templates/errors/500-dev.html [new file with mode: 0644]
src/foundation/api/revel/templates/errors/500.html [new file with mode: 0644]
src/foundation/api/revel/templates/errors/500.json [new file with mode: 0644]
src/foundation/api/revel/templates/errors/500.txt [new file with mode: 0644]
src/foundation/api/revel/templates/errors/500.xml [new file with mode: 0644]
src/foundation/api/revel/testdata/app/views/footer.html [new file with mode: 0644]
src/foundation/api/revel/testdata/app/views/header.html [new file with mode: 0644]
src/foundation/api/revel/testdata/app/views/hotels/show.html [new file with mode: 0644]
src/foundation/api/revel/testdata/conf/app.conf [new file with mode: 0644]
src/foundation/api/revel/testdata/conf/routes [new file with mode: 0644]
src/foundation/api/revel/testdata/i18n/config/test_app.conf [new file with mode: 0644]
src/foundation/api/revel/testdata/i18n/messages/dutch_messages.nl [new file with mode: 0644]
src/foundation/api/revel/testdata/i18n/messages/english_messages.en [new file with mode: 0644]
src/foundation/api/revel/testdata/i18n/messages/english_messages2.en [new file with mode: 0644]
src/foundation/api/revel/testdata/i18n/messages/invalid_message_file_name.txt [new file with mode: 0644]
src/foundation/api/revel/testdata/public/js/sessvars.js [new file with mode: 0644]
src/foundation/api/revel/testing/testsuite.go [new file with mode: 0644]
src/foundation/api/revel/testing/testsuite_test.go [new file with mode: 0644]
src/foundation/api/revel/util.go [new file with mode: 0644]
src/foundation/api/revel/util_test.go [new file with mode: 0644]
src/foundation/api/revel/utils/simplestack.go [new file with mode: 0644]
src/foundation/api/revel/utils/simplestack_test.go [new file with mode: 0644]
src/foundation/api/revel/validation.go [new file with mode: 0644]
src/foundation/api/revel/validation_test.go [new file with mode: 0644]
src/foundation/api/revel/validators.go [new file with mode: 0644]
src/foundation/api/revel/validators_test.go [new file with mode: 0644]
src/foundation/api/revel/version.go [new file with mode: 0644]
src/foundation/api/revel/watcher.go [new file with mode: 0644]

diff --git a/src/foundation/api/revel/.codebeatsettings b/src/foundation/api/revel/.codebeatsettings
new file mode 100644 (file)
index 0000000..f921ed8
--- /dev/null
@@ -0,0 +1,11 @@
+{
+  "GOLANG": {
+  "ABC":[15, 25, 50, 70],
+  "BLOCK_NESTING":[5, 6, 7, 8],
+  "CYCLO":[20, 30, 45, 60],
+    "TOO_MANY_IVARS": [15, 18, 20, 25],
+    "TOO_MANY_FUNCTIONS": [20, 30, 40, 50],
+    "TOTAL_COMPLEXITY": [150, 250, 400, 500],
+    "LOC": [50, 75, 90, 120]
+  }
+}
\ No newline at end of file
diff --git a/src/foundation/api/revel/.travis.yml b/src/foundation/api/revel/.travis.yml
new file mode 100644 (file)
index 0000000..2c81536
--- /dev/null
@@ -0,0 +1,67 @@
+language: go
+
+go:
+  - "1.8.x"
+  - "1.9.x"
+  - "1.10.x"
+  - "1.11.x"
+  - "tip"
+
+os:
+  - linux
+  - osx
+  - windows
+
+sudo: false
+
+branches:
+  only:
+    - master
+    - develop
+
+services:
+  # github.com/revel/revel/cache
+  - memcache
+  - redis-server
+
+before_install:
+  # TRAVIS_OS_NAME - linux and osx
+  - echo $TRAVIS_OS_NAME
+  - echo $PATH
+  - |
+    if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
+      brew update && brew install memcached redis && brew services start redis && brew services start memcached
+    fi
+  - |
+    if [[ "$TRAVIS_OS_NAME" != "windows" ]]; then
+      redis-server --daemonize yes
+      redis-cli info
+    else
+      # redis-server.exe
+      # redis-cli.exe info
+      echo $PATH
+    fi
+
+install:
+  # Setting environments variables
+  - export PATH=$PATH:$HOME/gopath/bin
+  - export REVEL_BRANCH="develop"
+  - 'if [[ "$TRAVIS_BRANCH" == "master" ]]; then export REVEL_BRANCH="master"; fi'
+  - 'echo "Travis branch: $TRAVIS_BRANCH, Revel dependency branch: $REVEL_BRANCH"'
+  - git clone -b $REVEL_BRANCH git://github.com/revel/modules ../modules/
+  - git clone -b $REVEL_BRANCH git://github.com/revel/cmd ../cmd/
+  - git clone -b $REVEL_BRANCH git://github.com/revel/config ../config/
+  - git clone -b $REVEL_BRANCH git://github.com/revel/cron ../cron/
+  - git clone -b $REVEL_BRANCH git://github.com/revel/examples ../examples/
+  - go get -t -v github.com/revel/revel/...
+  - go get -t -v github.com/revel/cmd/revel
+
+script:
+  - go test -v github.com/revel/revel/...
+
+matrix:
+  allow_failures:
+    - go: tip
+    - os: windows
+    - go: 1.6
+      os: osx
diff --git a/src/foundation/api/revel/AUTHORS b/src/foundation/api/revel/AUTHORS
new file mode 100644 (file)
index 0000000..d265544
--- /dev/null
@@ -0,0 +1 @@
+# TODO Revel Framework Authors Information
diff --git a/src/foundation/api/revel/CHANGELOG.md b/src/foundation/api/revel/CHANGELOG.md
new file mode 100644 (file)
index 0000000..5273460
--- /dev/null
@@ -0,0 +1,623 @@
+# CHANGELOG
+
+## v0.19.0
+# Release 0.19.0
+
+# Maintenance Release
+
+This release is focused on improving the security and resolving some issues. 
+
+**There are no breaking changes from version 0.18**
+
+[[revel/cmd](https://github.com/revel/cmd)]
+* Improved vendor folder detection revel/cmd#117
+* Added ordering of controllers so order remains consistent in main.go revel/cmd#112
+* Generate same value of `AppVersion` regardless of where Revel is run revel/cmd#108
+* Added referrer policy security header revel/cmd#114
+
+[[revel/modules](https://github.com/revel/modules)]
+* Added directory representation to static module revel/modules#46
+* Gorp enhancements (added abstraction layer for transactions and database connection so both can be used), Added security fix for CSRF module revel/modules#68
+* Added authorization configuration options to job page revel/modules#44
+
+[[revel/examples](https://github.com/revel/examples)]
+* General improvements and examples added revel/examples#39  revel/examples#40
+
+## v0.18
+# Release 0.18
+
+## Upgrade path
+The main breaking change is the removal of `http.Request` from the `revel.Request` object.
+Everything else should just work....
+
+## New items
+* Server Engine revel/revel#998
+The server engine implementation is described in the [docs](http://revel.github.io/manual/server-engine.html)
+* Allow binding to a structured map. revel/revel#998 
+Have a structure inside a map object which will be realized properly from params
+* Gorm module revel/modules/#51
+Added transaction controller
+* Gorp module revel/modules/#52
+* Autorun on startup in develop mode revel/cmd#95
+Start the application without doing a request first using revel run ....
+* Logger update revel/revel#1213
+Configurable logger and added context logging on controller via controller.Log
+* Before after finally panic controller method detection revel/revel#1211 
+Controller methods will be automatically detected and called - similar to interceptors but without the extra code
+* Float validation revel/revel#1209
+Added validation for floats
+* Timeago template function revel/revel#1207
+Added timeago function to Revel template functions
+* Authorization to jobs module revel/module#44
+Added ability to specify authorization to access the jobs module routes
+* Add MessageKey, ErrorKey methods to ValidationResult object revel/revel#1215
+This allows the message translator to translate the keys added. So model objects can send out validation codes
+* Vendor friendlier - Revel recognizes and uses `deps` (to checkout go libraries) if a vendor folder exists in the project root. 
+* Updated examples to use Gorp modules and new loggers
+
+
+### Breaking Changes
+
+* `http.Request` is no longer contained in `revel.Request` revel.Request remains functionally the same but 
+you cannot extract the `http.Request` from it. You can get the `http.Request` from `revel.Controller.Request.In.GetRaw().(*http.Request)`
+* `http.Response.Out` Is not the http.Response and is deprecated, you can get the output writer by doing `http.Response.GetWriter()`. You can get the `http.Response` from revel.Controller.Response.Out.Server.GetRaw().(*http.Response)`
+
+* `Websocket` changes. `revel.ServerWebsocket` is the new type of object you need to declare for controllers 
+which should need to attach to websockets. Implementation of these objects have been simplified
+
+Old
+```
+
+func (c WebSocket) RoomSocket(user string, ws *websocket.Conn) revel.Result {
+       // Join the room.
+       subscription := chatroom.Subscribe()
+       defer subscription.Cancel()
+
+       chatroom.Join(user)
+       defer chatroom.Leave(user)
+
+       // Send down the archive.
+       for _, event := range subscription.Archive {
+               if websocket.JSON.Send(ws, &event) != nil {
+                       // They disconnected
+                       return nil
+               }
+       }
+
+       // In order to select between websocket messages and subscription events, we
+       // need to stuff websocket events into a channel.
+       newMessages := make(chan string)
+       go func() {
+               var msg string
+               for {
+                       err := websocket.Message.Receive(ws, &msg)
+                       if err != nil {
+                               close(newMessages)
+                               return
+                       }
+                       newMessages <- msg
+               }
+       }()
+```
+New
+```
+func (c WebSocket) RoomSocket(user string, ws revel.ServerWebSocket) revel.Result {
+       // Join the room.
+       subscription := chatroom.Subscribe()
+       defer subscription.Cancel()
+
+       chatroom.Join(user)
+       defer chatroom.Leave(user)
+
+       // Send down the archive.
+       for _, event := range subscription.Archive {
+               if ws.MessageSendJSON(&event) != nil {
+                       // They disconnected
+                       return nil
+               }
+       }
+
+       // In order to select between websocket messages and subscription events, we
+       // need to stuff websocket events into a channel.
+       newMessages := make(chan string)
+       go func() {
+               var msg string
+               for {
+                       err := ws.MessageReceiveJSON(&msg)
+                       if err != nil {
+                               close(newMessages)
+                               return
+                       }
+                       newMessages <- msg
+               }
+       }()
+```
+* GORM module has been refactored into modules/orm/gorm 
+
+
+### Deprecated methods
+* `revel.Request.FormValue()` Is deprecated, you should use methods in the controller.Params to access this data
+* `revel.Request.PostFormValue()` Is deprecated, you should use methods in the controller.Params.Form to access this data
+* `revel.Request.ParseForm()` Is deprecated - not needed
+* `revel.Request.ParseMultipartForm()` Is deprecated - not needed
+* `revel.Request.Form` Is deprecated, you should use the controller.Params.Form to access this data
+* `revel.Request.MultipartForm` Is deprecated, you should use the controller.Params.Form to access this data
+* `revel.TRACE`, `revel.INFO` `revel.WARN` `revel.ERROR` are deprecated. Use new application logger `revel.AppLog` and the controller logger `controller.Log`. See [logging](http://revel.github.io/manual/logging.html) for more details.
+
+### Features
+
+* Pluggable server engine support. You can now implement **your own server engine**. This means if you need to listen to more then 1 IP address or port you can implement a custom server engine to do this. By default Revel uses GO http server, but also available is fasthttp server in the revel/modules repository. See the docs for more information on how to implement your own engine.
+
+### Enhancements
+* Controller instances are cached for reuse. This speeds up the request response time and prevents unnecessary garbage collection cycles.  
+
+### Bug fixes
+
+
+
+
+## v0.17
+
+[[revel/revel](https://github.com/revel/revel)]
+
+* add-validation
+* i18-lang-by-param
+* Added namespace to routes, controllers
+* Added go 1.6 to testing
+* Adds the ability to set the language by a url parameter. The route file will need to specify the parameter so that it will be picked up
+* Changed url validation logic to regex
+* Added new validation mehtods (IPAddr,MacAddr,Domain,URL,PureText)
+
+[[revel/cmd](https://github.com/revel/cmd)]
+
+* no changes
+
+[[revel/config](https://github.com/revel/config)]
+
+* no changes
+
+[[revel/modules](https://github.com/revel/modules)]
+
+* Added Gorm module
+
+[[revel/cron](https://github.com/revel/cron)]
+
+* Updated cron task manager
+* Added ability to run a specific job, reschedules job if cron is running.
+
+[[revel/examples](https://github.com/revel/examples)]
+
+* Gorm module (Example)
+
+# v0.16.0
+
+Deprecating support for golang versions prior to 1.6
+### Breaking Changes
+
+* `CurrentLocaleRenderArg` to `CurrentLocaleViewArg` for consistency
+* JSON requests are now parsed by Revel, if the content type is `text/json` or `application/json`. The raw data is available in `Revel.Controller.Params.JSON`. But you can also use the automatic controller operation to load the data like you would any structure or map. See [here](http://revel.github.io/manual/parameters.html) for more details
+
+### Features
+
+* Modular Template Engine #1170 
+* Pongo2 engine driver added revel/modules#39
+* Ace engine driver added revel/modules#40
+* Added i18n template support #746 
+
+### Enhancements
+
+* JSON request binding #1161 
+* revel.SetSecretKey function added #1127 
+* ResolveFormat now looks at the extension as well (this sets the content type) #936 
+* Updated command to run tests using the configuration revel/cmd#61
+
+### Bug fixes
+
+* Updated documentation typos revel/modules#37
+* Updated order of parameter map assignment #1155 
+* Updated cookie lifetime for firefox #1174 
+* Added test path for modules, so modules will run tests as well #1162 
+* Fixed go profiler module revel/modules#20
+
+
+# v0.15.0
+@shawncatz released this on 2017-05-11
+
+Deprecating support for golang versions prior to 1.7
+
+### Breaking Changes
+
+* None
+
+### Features
+
+* None
+
+### Enhancements
+
+* Update and improve docs revel/examples#17 revel/cmd#85
+
+### Bug fixes
+
+* Prevent XSS revel/revel#1153
+* Improve error checking for go version detection revel/cmd#86
+
+# v0.14.0
+@notzippy released this on 2017-03-24
+
+## Changes since v0.13.0
+
+#### Breaking Changes
+- `revel/revel`:
+  - change RenderArgs to ViewArgs PR #1135
+  - change RenderJson to RenderJSON PR #1057
+  - change RenderHtml to RenderHTML PR #1057
+  - change RenderXml to RenderXML PR #1057
+
+#### Features
+- `revel/revel`:
+
+#### Enhancements
+- `revel/revel`:
+
+
+#### Bug Fixes
+- `revel/revel`:
+
+
+# v0.13.1
+@jeevatkm released this on 2016-06-07
+
+**Bug fix:**
+- Windows path fix #1064
+
+
+# v0.13.0
+@jeevatkm released this on 2016-06-06
+
+## Changes since v0.12.0
+
+#### Breaking Changes
+- `revel/revel`:
+  - Application Config name changed from `watcher.*` to `watch.*`  PR #992, PR #991
+
+#### Features
+- `revel/revel`:
+  - Request access log PR #1059, PR #913, #1055
+  - Messages loaded from modules too PR #828
+- `revel/cmd`:
+  - Added `revel version` command emits the revel version and go version revel/cmd#19
+
+#### Enhancements
+- `revel/revel`:
+  - Creates log directory if missing PR #1039
+  - Added `application/javascript` to accepted headers PR #1022
+  - You can change `Server.Addr` value via hook function PR #999
+  - Improved deflate/gzip compressor PR #995
+  - Consistent config name `watch.*` PR #992, PR #991
+  - Defaults to HttpOnly and always secure cookies for non-dev mode #942, PR #943
+  - Configurable server Read and Write Timeout via app config #936, PR #940
+  - `OnAppStart` hook now supports order param too PR #935
+  - Added `PutForm` and `PutFormCustom` helper method in `testing.TestSuite` #898
+  - Validator supports UTF-8 string too PR #891, #841
+  - Added `InitServer` method that returns `http.HandlerFunc` PR #879
+  - Symlink aware processing Views, Messages and Watch mode PR #867, #673
+  - Added i18n settings support unknown format PR #852
+  - i18n: Make Message Translation pluggable PR #768
+  - jQuery `min-2.2.4` & Bootstrap `min-3.3.6` version updated in `skeleton/public` #1063
+- `revel/cmd`:
+  - Revel identifies current `GOPATH` and performs `new` command; relative to directory revel/revel#1004
+  - Installs package dependencies during a build PR revel/cmd#43
+  - Non-200 response of test case request will correctly result into error PR revel/cmd#38
+  - Websockets SSL support in `dev` mode PR revel/cmd#32
+  - Won't yell about non-existent directory while cleaning PR revel/cmd#31, #908
+    - [x] non-fatal errors when building #908
+  - Improved warnings about route generation PR revel/cmd#25
+  - Command is Symlink aware PR revel/cmd#20
+  - `revel package` & `revel build` now supports environment mode PR revel/cmd#14
+  - `revel clean` now cleans generated routes too PR revel/cmd#6
+- `revel/config`:
+  - Upstream `robfig/config` refresh and import path updated from `github.com/revel/revel/config` to `github.com/revel/config`, PR #868
+  - Config loading order and external configuration to override application configuration revel/config#4 [commit](https://github.com/revel/revel/commit/f3a422c228994978ae0a5dd837afa97248b26b41)
+  - Application config error will produce insight on error PR revel/config#3 [commit](https://github.com/revel/config/commit/85a123061070899a82f59c5ef6187e8fb4457f64)
+- `revel/modules`:
+  - Testrunner enhancements
+    - Minor improvement on testrunner module PR #820, #895
+    - Add Test Runner panels per test group PR revel/modules#12
+- `revel/revel.github.io`:
+  - Update `index.md` and homepage (change how samples repo is installed) PR [#85](https://github.com/revel/revel.github.io/pull/85)
+  - Couple of UI improvements PR [#93](https://github.com/revel/revel.github.io/pull/93)
+  - Updated techempower benchmarks Round 11 [URL](http://www.techempower.com/benchmarks/#section=data-r11)
+  - Docs updated for v0.13 release
+- Cross-Platform Support
+  - Slashes should be normalized in paths #260, PR #1028, PR #928
+
+#### Bug Fixes
+- `revel/revel`:
+  - Binder: Multipart `io.Reader` parameters needs to be closed #756
+  - Default Date & Time Format correct in skeleton PR #1062, #878
+  - Addressed with alternative for `json: unsupported type: <-chan struct {}` on Go 1.6 revel/revel#1037
+  - Addressed one edge case, invalid Accept-Encoding header causes panic revel/revel#914
+
+
+# v0.11.3
+@brendensoares released this on 2015-01-04
+
+This is a minor release to address a critical bug (#824) in v0.11.2.
+
+Everybody is strongly encouraged to rebuild their projects with the latest version of Revel. To do it, execute the commands:
+
+``` sh
+$ go get -u github.com/revel/cmd/revel
+
+$ revel build github.com/myusername/myproject /path/to/destination/folder
+```
+
+
+# v0.11.2
+on 2014-11-23
+
+This is a minor release to address a critical bug in v0.11.0.
+
+Everybody is strongly encouraged to rebuild their projects with the latest version of Revel. To do it, execute the commands:
+
+``` sh
+$ go get -u github.com/revel/cmd/revel
+
+$ revel build github.com/myusername/myproject /path/to/destination/folder
+```
+
+
+# v0.11.1
+@pushrax released this on 2014-10-27
+
+This is a minor release to address a compilation error in v0.11.0.
+
+
+# v0.12.0
+@brendensoares released this on 2015-03-25
+
+Changes since v0.11.3:
+
+## Breaking Changes
+1. Add import path to new `testing` sub-package for all Revel tests. For example:
+
+``` go
+package tests
+
+import "github.com/revel/revel/testing"
+
+type AppTest struct {
+    testing.TestSuite
+}
+```
+1. We've relocated modules to a dedicated repo. Make sure you update your `conf/app.conf`. For example, change:
+
+``` ini
+module.static=github.com/revel/revel/modules/static
+module.testrunner = github.com/revel/revel/modules/testrunner
+```
+
+to the new paths:
+
+``` ini
+module.static=github.com/revel/modules/static
+module.testrunner = github.com/revel/modules/testrunner
+```
+
+## [ROADMAP] Focus: Improve Internal Organization
+
+The majority of our effort here is increasing the modularity of the code within Revel so that further development can be done more productively while keeping documentation up to date.
+- `revel/revel.github.io`
+  - [x] Improve docs #[43](https://github.com/revel/revel.github.io/pull/43)
+- `revel/revel`:
+  - [x] Move the `revel/revel/harness` to the `revel/cmd` repo since it's only used during build time. #[714](https://github.com/revel/revel/issues/714)
+  - [x] Move `revel/revel/modules` to the `revel/modules` repo #[785](https://github.com/revel/revel/issues/785)
+  - [x] Move `revel/revel/samples` to the `revel/samples` repo #[784](https://github.com/revel/revel/issues/784)
+  - [x] `testing` TestSuite #[737](https://github.com/revel/revel/issues/737) #[810](https://github.com/revel/revel/issues/810)
+  - [x] Feature/sane http timeout defaults #[837](https://github.com/revel/revel/issues/837) PR#[843](https://github.com/revel/revel/issues/843) Bug Fix PR#[860](https://github.com/revel/revel/issues/860)
+  - [x] Eagerly load templates in dev mode #[353](https://github.com/revel/revel/issues/353) PR#[844](https://github.com/revel/revel/pull/844)
+  - [x] Add an option to trim whitespace from rendered HTML #[800](https://github.com/revel/revel/issues/800)
+  - [x] Remove built-in mailer in favor of 3rd party package #[783](https://github.com/revel/revel/issues/783)
+  - [x] Allow local reverse proxy access to jobs module status page for IPv4/6 #[481](https://github.com/revel/revel/issues/481) PR#[6](https://github.com/revel/modules/pull/6) PR#[7](https://github.com/revel/modules/pull/7)
+  - [x] Add default http.Status code for render methods. #[728](https://github.com/revel/revel/issues/728)
+  - [x] add domain for cookie #[770](https://github.com/revel/revel/issues/770) PR#[882](https://github.com/revel/revel/pull/882)
+  - [x] production mode panic bug #[831](https://github.com/revel/revel/issues/831) PR#[881](https://github.com/revel/revel/pull/881)
+  - [x] Fixes template loading order whether watcher is enabled or not #[844](https://github.com/revel/revel/issues/844)
+  - [x] Fixes reverse routing wildcard bug PR#[886](https://github.com/revel/revel/pull/886) #[869](https://github.com/revel/revel/issues/869)
+  - [x] Fixes router app start bug without routes. PR #[855](https://github.com/revel/revel/pull/855)
+  - [x] Friendly URL template errors; Fixes template `url` func "index out of range" when param is `undefined` #[811](https://github.com/revel/revel/issues/811) PR#[880](https://github.com/revel/revel/pull/880)
+  - [x] Make result compression conditional PR#[888](https://github.com/revel/revel/pull/888)
+  - [x] ensure routes are loaded before returning from OnAppStart callback PR#[884](https://github.com/revel/revel/pull/884)
+  - [x] Use "302 Found" HTTP code for redirect PR#[900](https://github.com/revel/revel/pull/900)
+  - [x] Fix broken fake app tests PR#[899](https://github.com/revel/revel/pull/899)
+  - [x] Optimize search of template names PR#[885](https://github.com/revel/revel/pull/885)
+- `revel/cmd`:
+  - [x] track current Revel version #[418](https://github.com/revel/revel/issues/418) PR#[858](https://github.com/revel/revel/pull/858)
+  - [x] log path error After revel build? #[763](https://github.com/revel/revel/issues/763)
+  - [x] Use a separate directory for revel project binaries #[17](https://github.com/revel/cmd/pull/17) #[819](https://github.com/revel/revel/issues/819)
+  - [x] Overwrite generated app files instead of deleting directory #[551](https://github.com/revel/revel/issues/551) PR#[23](https://github.com/revel/cmd/pull/23)
+- `revel/modules`:
+  - [x] Adds runtime pprof/trace support #[9](https://github.com/revel/modules/pull/9)
+- Community Goals:
+  - [x] Issue labels #[545](https://github.com/revel/revel/issues/545)
+    - [x] Sync up labels/milestones in other repos #[721](https://github.com/revel/revel/issues/721)
+  - [x] Update the Revel Manual to reflect current features
+    - [x] [revel/revel.github.io/32](https://github.com/revel/revel.github.io/issues/32)
+    - [x] [revel/revel.github.io/39](https://github.com/revel/revel.github.io/issues/39)
+    - [x] Docs are obsolete, inaccessible TestRequest.testSuite #[791](https://github.com/revel/revel/issues/791)
+    - [x] Some questions about revel & go docs #[793](https://github.com/revel/revel/issues/793)
+  - [x] RFCs to organize features #[827](https://github.com/revel/revel/issues/827)
+
+[Full list of commits](https://github.com/revel/revel/compare/v0.11.3...v0.12.0)
+
+
+# v0.11.0
+@brendensoares released this on 2014-10-26
+
+Note, Revel 0.11 requires Go 1.3 or higher.
+
+Changes since v0.10:
+
+[BUG]   #729    Adding define inside the template results in an error (Changes how template file name case insensitivity is handled)
+
+[ENH]   #769    Add swap files to gitignore
+[ENH]   #766    Added passing in build flags to the go build command
+[ENH]   #761    Fixing cross-compiling issue #456 setting windows path from linux
+[ENH]   #759    Include upload sample's tests in travis
+[ENH]   #755    Changes c.Action to be the action method name's letter casing per #635
+[ENH]   #754    Adds call stack display to runtime panic in browser to match console
+[ENH]   #740    Redis Cache: Add timeouts.
+[ENH]   #734    watcher: treat fsnotify Op as a bitmask
+[ENH]   #731    Second struct in type revel fails to find the controller
+[ENH]   #725    Testrunner: show response info
+[ENH]   #723    Improved compilation errors and open file from error page
+[ENH]   #720    Get testrunner path from config file
+[ENH]   #707    Add log.colorize option to enable/disable colorize
+[ENH]   #696    Revel file upload testing
+[ENH]   #694    Install dependencies at build time
+[ENH]   #693    Prefer extension over Accept header
+[ENH]   #692    Update fsnotify to v1 API
+[ENH]   #690    Support zero downtime restarts
+[ENH]   #687    Tests: request override
+[ENH]   #685    Persona sample tests and bugfix
+[ENH]   #598    Added README file to Revel skeleton
+[ENH]   #591    Realtime rebuild
+[ENH]   #573    Add AppRoot to allow changing the root path of an application
+
+[FTR]   #606    CSRF Support
+
+[Full list of commits](https://github.com/revel/revel/compare/v0.10.0...v0.11.0)
+
+
+# v0.10.0
+@brendensoares released this on 2014-08-10
+
+Changes since v0.9.1:
+- [FTR] #641 - Add "X-HTTP-Method-Override" to router
+- [FTR] #583 - Added HttpMethodOverride filter to routes
+- [FTR] #540 - watcher flag for refresh on app start
+- [BUG] #681 - Case insensitive comparison for websocket upgrades (Fixes IE Websockets ...
+- [BUG] #668 - Compression: Properly close gzip/deflate
+- [BUG] #667 - Fix redis GetMulti and improve test coverage
+- [BUG] #664 - Is compression working correct?
+- [BUG] #657 - Redis Cache: panic when testing Ge
+- [BUG] #637 - RedisCache: fix Get/GetMulti error return
+- [BUG] #621 - Bugfix/router csv error
+- [BUG] #618 - Router throws exception when parsing line with multiple default string arguments
+- [BUG] #604 - Compression: Properly close gzip/deflate.
+- [BUG] #567 - Fixed regex pattern to properly require message files to have a dot in filename
+- [BUG] #566 - Compression fails ("unexpected EOF" in tests)
+- [BUG] #287 - Don't remove the parent folders containing generated code.
+- [BUG] #556 - fix for #534, also added url path to not found message
+- [BUG] #534 - Websocket route not found
+- [BUG] #343 - validation.Required(funtionCall).Key(...) - reflect.go:715: Failed to generate name for field.
+- [ENH] #643 - Documentation Fix in Skeleton for OnAppStart
+- [ENH] #674 - Removes custom `eq` template function
+- [ENH] #669 - Develop compress closenotifier
+- [ENH] #663 - fix for static content type not being set and defaulting to OS
+- [ENH] #658 - Minor: fix niggle with import statement
+- [ENH] #652 - Update the contributing guidelines
+- [ENH] #651 - Use upstream gomemcache again
+- [ENH] #650 - Go back to upstream memcached library
+- [ENH] #612 - Fix CI package error
+- [ENH] #611 - Fix "go vet" problems
+- [ENH] #610 - Added MakeMultipartRequest() to the TestSuite
+- [ENH] #608 - Develop compress closenotifier
+- [ENH] #596 - Expose redis cache options to config
+- [ENH] #581 - Make the option template tag type agnostic.
+- [ENH] #576 - Defer session instantiation to first set
+- [ENH] #565 - Fix #563 -- Some custom template funcs cannot be used in JavaScript cont...
+- [ENH] #563 - TemplateFuncs cannot be used in JavaScript context
+- [ENH] #561 - Fix missing extension from message file causing panic
+- [ENH] #560 - enhancement / templateFunc `firstof`
+- [ENH] #555 - adding symlink handling to the template loader and watcher processes
+- [ENH] #531 - Update app.conf.template
+- [ENH] #520 - Respect controller's Response.Status when action returns nil
+- [ENH] #519 - Link to issues
+- [ENH] #486 - Support for json compress
+- [ENH] #480 - Eq implementation in template.go still necessary ?
+- [ENH] #461 - Cron jobs not started until I pull a page
+- [ENH] #323 - disable session/set-cookie for `Static.Serve()`
+
+[Full list of commits](https://github.com/revel/revel/compare/v0.9.1...v0.10.0)
+
+
+# v0.9.1
+@pushrax released this on 2014-03-02
+
+Minor patch release to address a couple bugs.
+
+Changes since v0.9.0:
+- [BUG] #529 - Wrong path was used to determine existence of `.git`
+- [BUG] #532 - Fix typo for new type `ValidEmail`
+
+The full list of commits can be found [here](https://github.com/revel/revel/compare/v0.9.0...v0.9.1).
+
+
+# v0.9.0
+@pushrax released this on 2014-02-26
+
+## Revel GitHub Organization
+
+We've moved development of the framework to the @revel GitHub organization, to help manage the project as Revel grows. The old import path is still valid, but will not be updated in the future.
+
+You'll need to manually update your apps to work with the new import path. This can be done by replacing all instances of `github.com/robfig/revel` with `github.com/revel/revel` in your app, and running:
+
+```
+$ cd your_app_folder
+$ go get -u github.com/howeyc/fsnotify  # needs updating
+$ go get github.com/revel/revel
+$ go get github.com/revel/cmd/revel     # command line tools have moved
+```
+
+**Note:** if you have references to `github.com/robfig/revel/revel` in any files, you need to replace them with `github.com/revel/cmd/revel` _before_ replacing `github.com/robfig/revel`! (note the prefix collision)
+
+If you have any trouble upgrading or notice something we missed, feel free to hop in the IRC channel (#revel on Freenode) or send the mailing list a message.
+
+Also note, the documentation is now at [revel.github.io](http://revel.github.io)!
+
+Changes since v0.8:
+- [BUG] #522 - `revel new` bug
+- [BUG] - Booking sample error
+- [BUG] #504 - File access via URL security issue
+- [BUG] #489 - Email validator bug
+- [BUG] #475 - File watcher infinite loop
+- [BUG] #333 - Extensions in routes break parameters
+- [FTR] #472 - Support for 3rd part app skeletons
+- [ENH] #512 - Per session expiration methods
+- [ENH] #496 - Type check renderArgs[CurrentLocalRenderArg]
+- [ENH] #490 - App.conf manual typo
+- [ENH] #487 - Make files executable on `revel build`
+- [ENH] #482 - Retain input values after form valdiation
+- [ENH] #473 - OnAppStart documentation
+- [ENH] #466 - JSON error template quoting fix
+- [ENH] #464 - Remove unneeded trace statement
+- [ENH] #457 - Remove unneeded trace
+- [ENH] #508 - Support arbitrary network types
+- [ENH] #516 - Add Date and Message-Id mail headers
+
+The full list of commits can be found [here](https://github.com/revel/revel/compare/v0.8...v0.9.0).
+
+
+# v0.8
+@pushrax released this on 2014-01-06
+
+Changes since v0.7:
+- [BUG] #379 - HTTP 500 error for not found public path files
+- [FTR] #424 - HTTP pprof support
+- [FTR] #346 - Redis Cache support
+- [FTR] #292 - SMTP Mailer
+- [ENH] #443 - Validator constructors to improve `v.Check()` usage
+- [ENH] #439 - Basic terminal output coloring
+- [ENH] #428 - Improve error message for missing `RenderArg`
+- [ENH] #422 - Route embedding for modules
+- [ENH] #413 - App version variable
+- [ENH] #153 - $GOPATH-wide file watching aka hot loading
+
+
+# v0.6
+@robfig released this on 2013-09-16
+
+
+
diff --git a/src/foundation/api/revel/CONTRIBUTING.md b/src/foundation/api/revel/CONTRIBUTING.md
new file mode 100644 (file)
index 0000000..5f699ca
--- /dev/null
@@ -0,0 +1,162 @@
+## Contributing to Revel
+
+This describes how developers may contribute to Revel.
+
+## Mission
+
+Revel's mission is to provide a batteries-included framework for making large
+scale web application development as efficient and maintainable as possible.
+
+The design should be configurable and modular so that it can grow with the
+developer. However, it should provide a wonderful un-boxing experience and
+default configuration that can woo new developers and make simple web apps
+straight forward. The framework should have an opinion about how to do all of the
+common tasks in web development to reduce unnecessary cognitive load.
+
+Perhaps most important of all, Revel should be a joy to use. We want to reduce
+the time spent on tedious boilerplate functionality and increase the time
+available for creating polished solutions for your application's target users.
+
+## How to Contribute
+
+### Join the Community
+
+The first step to making Revel better is joining the community! You can find the
+community on:
+
+* [Google Groups](https://groups.google.com/forum/#!forum/revel-framework) via [revel-framework@googlegroups.com](mailto:revel-framework@googlegroups.com)
+* [GitHub Issues](https://github.com/revel/revel/issues)
+* [StackOverflow Questions](http://stackoverflow.com/questions/tagged/revel)
+* [IRC](http://webchat.freenode.net/?channels=%23revel&uio=d4) via #revel on Freenode
+
+Once you've joined, there are many ways to contribute to Revel:
+
+* Report bugs (via GitHub)
+* Answer questions of other community members (via Google Groups or IRC)
+* Give feedback on new feature discussions (via GitHub and Google Groups)
+* Propose your own ideas (via Google Groups or GitHub)
+
+### How Revel is Developed
+
+We have begun to formalize the development process by adopting pragmatic
+practices such as:
+
+* Developing on the `develop` branch
+* Merging `develop` branch to `master` branch in 6 week iterations
+* Tagging releases with MAJOR.MINOR syntax (e.g. v0.8)
+** We may also tag MAJOR.MINOR.HOTFIX releases as needed (e.g. v0.8.1) to
+address urgent bugs. Such releases will not introduce or change functionality
+* Managing bugs, enhancements, features and release milestones via GitHub's Issue Tracker
+* Using feature branches to create pull requests
+* Discussing new features **before** hacking away at it
+
+
+### How to Correctly Fork
+
+Go uses the repository URL to import packages, so forking and `go get`ing the
+forked project **will not work**.
+
+Instead, follow these steps:
+
+1. Install Revel normally
+2. Fork Revel on GitHub
+3. Add your fork as a git remote
+
+Here's the commands to do so:
+```
+$ go get github.com/revel/revel                        # Install Revel
+$ cd $GOPATH/src/github.com/revel/revel                # Change directory to revel repo
+$ git remote add fork git@github.com:$USER/revel.git  # Add your fork as a remote, where $USER is your GitHub username
+```
+
+### Create a Feature Branch & Code Away!
+
+Now that you've properly installed and forked Revel, you are ready to start coding (assuming
+you have a validated your ideas with other community members)!
+
+In order to have your pull requests accepted, we recommend you make your changes to Revel on a
+new git branch. For example,
+```
+$ git checkout -b feature/useful-new-thing origin/develop    # Create a new branch based on develop and switch to it
+$ ...                                                        # Make your changes and commit them
+$ git push fork feature/useful-new-thing                     # After new commits, push to your fork
+```
+
+### Format Your Code
+
+Remember to run `go fmt` before committing your changes.
+Many Go developers opt to have their editor run `go fmt` automatically when
+saving Go files.
+
+Additionally, follow the [core Go style conventions](https://code.google.com/p/go-wiki/wiki/CodeReviewComments)
+to have your pull requests accepted.
+
+### Write Tests (and Benchmarks for Bonus Points)
+
+Significant new features require tests. Besides unit tests, it is also possible
+to test a feature by exercising it in one of the sample apps and verifying its
+operation using that app's test suite. This has the added benefit of providing
+example code for developers to refer to.
+
+Benchmarks are helpful but not required.
+
+### Run the Tests
+
+Typically running the main set of unit tests will be sufficient:
+
+```
+$ go test github.com/revel/revel
+```
+
+Refer to the
+[Travis configuration](https://github.com/revel/revel/blob/master/.travis.yml)
+for the full set of tests.  They take less than a minute to run.
+
+### Document Your Feature
+
+Due to the wide audience and shared nature of Revel, documentation is an essential
+addition to your new code. **Pull requests risk not being accepted** until proper
+documentation is created to detail how to make use of new functionality.
+
+The [Revel web site](http://revel.github.io/) is hosted on GitHub Pages and
+[built with Jekyll](https://help.github.com/articles/using-jekyll-with-pages).
+
+To develop the Jekyll site locally:
+
+    # Clone the documentation repository
+    $ git clone git@github.com:revel/revel.github.io
+    $ cd revel.github.io
+
+    # Install and run Jekyll to generate and serve the site
+    $ gem install jekyll kramdown
+    $ jekyll serve --watch
+
+    # Now load in your browser
+    $ open http://localhost:4000/
+
+Any changes you make to the site should be reflected within a few seconds.
+
+### Submit Pull Request
+
+Once you've done all of the above & pushed your changes to your fork, you can create a pull request for review and acceptance.
+
+## Potential Projects
+
+These are outstanding feature requests, roughly ordered by priority.
+Additionally, there are frequently smaller feature requests or items in the
+[issues](https://github.com/revel/revel/issues?labels=contributor+ready&page=1&state=open).
+
+1.     Better ORM support.  Provide more samples (or modules) and better documentation for setting up common situations like SQL database, Mongo, LevelDB, etc.
+1.     Support for other templating languages (e.g. mustache, HAML).  Make TemplateLoader pluggable.  Use Pongo instead of vanilla Go templates (and update the samples)
+1.     Test Fixtures
+1.     Authenticity tokens for CSRF protection
+1.     Coffeescript pre-processor.  Could potentially use [otto](https://github.com/robertkrimen/otto) as a native Go method to compiling.
+1.     SCSS/LESS pre-processor.
+1.     GAE support.  Some progress made in the 'appengine' branch -- the remaining piece is running the appengine services in development.
+1.     More Form helpers (template funcs).
+1.     A Mongo module (perhaps with a sample app)
+1.     Deployment to OpenShift (support, documentation, etc)
+1.     Improve the logging situation.  The configuration is a little awkward and not very powerful.  Integrating something more powerful would be good. (like [seelog](https://github.com/cihub/seelog) or [log4go](https://code.google.com/p/log4go/))
+1.     ETags, cache controls
+1.     A module or plugins for adding HTTP Basic Auth
+1.     Allowing the app to hook into the source code processing step
diff --git a/src/foundation/api/revel/LICENSE b/src/foundation/api/revel/LICENSE
new file mode 100644 (file)
index 0000000..92246e3
--- /dev/null
@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (C) 2012-2018 The Revel Framework Authors.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/src/foundation/api/revel/README.md b/src/foundation/api/revel/README.md
new file mode 100644 (file)
index 0000000..c89e193
--- /dev/null
@@ -0,0 +1,56 @@
+# Revel Framework
+
+[![Build Status](https://secure.travis-ci.org/revel/revel.svg?branch=master)](http://travis-ci.org/revel/revel) 
+[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
+[![Go Report Card](https://goreportcard.com/badge/github.com/revel/revel)](https://goreportcard.com/report/github.com/revel/revel)
+
+A high productivity, full-stack web framework for the [Go language](http://www.golang.org).
+
+Current Version: 0.21.0 (2018-10-30)
+
+**Because of Default HTTP Server's graceful shutdown, Go 1.8+ is required.**
+
+## Quick Start
+
+Enter Go's path (format varies based on OS):
+
+       cd $GOPATH
+
+Install Revel:
+
+       go get -u github.com/revel/cmd/revel
+
+Create & Run your app:
+
+       revel new -a my-app -r
+
+Open http://localhost:9000 in your browser and you should see "It works!"
+
+
+## Community
+
+* [Gitter](https://gitter.im/revel/community)
+* [StackOverflow](http://stackoverflow.com/questions/tagged/revel)
+
+
+## Learn More
+
+* [Manual, Samples, Godocs, etc](http://revel.github.io)
+* [Apps using Revel](https://github.com/revel/revel/wiki/Apps-in-the-Wild)
+* [Articles Featuring Revel](https://github.com/revel/revel/wiki/Articles)
+
+## Contributing
+
+* [Contributing Code Guidelines](https://github.com/revel/revel/blob/master/CONTRIBUTING.md)
+* [Revel Contributors](https://github.com/revel/revel/graphs/contributors)
+
+## Contributors
+
+[![](https://sourcerer.io/fame/notzippy/revel/revel/images/0)](https://sourcerer.io/fame/notzippy/revel/revel/links/0)
+[![](https://sourcerer.io/fame/notzippy/revel/revel/images/1)](https://sourcerer.io/fame/notzippy/revel/revel/links/1)
+[![](https://sourcerer.io/fame/notzippy/revel/revel/images/2)](https://sourcerer.io/fame/notzippy/revel/revel/links/2)
+[![](https://sourcerer.io/fame/notzippy/revel/revel/images/3)](https://sourcerer.io/fame/notzippy/revel/revel/links/3)
+[![](https://sourcerer.io/fame/notzippy/revel/revel/images/4)](https://sourcerer.io/fame/notzippy/revel/revel/links/4)
+[![](https://sourcerer.io/fame/notzippy/revel/revel/images/5)](https://sourcerer.io/fame/notzippy/revel/revel/links/5)
+[![](https://sourcerer.io/fame/notzippy/revel/revel/images/6)](https://sourcerer.io/fame/notzippy/revel/revel/links/6)
+[![](https://sourcerer.io/fame/notzippy/revel/revel/images/7)](https://sourcerer.io/fame/notzippy/revel/revel/links/7)
diff --git a/src/foundation/api/revel/before_after_filter.go b/src/foundation/api/revel/before_after_filter.go
new file mode 100644 (file)
index 0000000..e9af1d3
--- /dev/null
@@ -0,0 +1,60 @@
+package revel
+
+import (
+       "reflect"
+)
+
+// Autocalls any defined before and after methods on the target controller
+// If either calls returns a value then the result is returned
+func BeforeAfterFilter(c *Controller, fc []Filter) {
+       defer func() {
+               if resultValue := beforeAfterFilterInvoke(FINALLY, c); resultValue != nil && !resultValue.IsNil() {
+                       c.Result = resultValue.Interface().(Result)
+               }
+       }()
+       defer func() {
+               if err := recover(); err != nil {
+                       if resultValue := beforeAfterFilterInvoke(PANIC, c); resultValue != nil && !resultValue.IsNil() {
+                               c.Result = resultValue.Interface().(Result)
+                       }
+                       panic(err)
+               }
+       }()
+       if resultValue := beforeAfterFilterInvoke(BEFORE, c); resultValue != nil && !resultValue.IsNil() {
+               c.Result = resultValue.Interface().(Result)
+       }
+       fc[0](c, fc[1:])
+       if resultValue := beforeAfterFilterInvoke(AFTER, c); resultValue != nil && !resultValue.IsNil() {
+               c.Result = resultValue.Interface().(Result)
+       }
+}
+
+func beforeAfterFilterInvoke(method When, c *Controller) (r *reflect.Value) {
+
+       if c.Type == nil {
+               return
+       }
+       var index []*ControllerFieldPath
+       switch method {
+       case BEFORE:
+               index = c.Type.ControllerEvents.Before
+       case AFTER:
+               index = c.Type.ControllerEvents.After
+       case FINALLY:
+               index = c.Type.ControllerEvents.Finally
+       case PANIC:
+               index = c.Type.ControllerEvents.Panic
+       }
+
+       if len(index) == 0 {
+               return
+       }
+       for _, function := range index {
+               result := function.Invoke(reflect.ValueOf(c.AppController), nil)[0]
+               if !result.IsNil() {
+                       return &result
+               }
+       }
+
+       return
+}
diff --git a/src/foundation/api/revel/binder.go b/src/foundation/api/revel/binder.go
new file mode 100644 (file)
index 0000000..a42ed21
--- /dev/null
@@ -0,0 +1,543 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "encoding/json"
+       "fmt"
+       "io"
+       "io/ioutil"
+       "mime/multipart"
+       "os"
+       "reflect"
+       "strconv"
+       "strings"
+       "time"
+)
+
+// A Binder translates between string parameters and Go data structures.
+type Binder struct {
+       // Bind takes the name and type of the desired parameter and constructs it
+       // from one or more values from Params.
+       //
+       // Example
+       //
+       // Request:
+       //   url?id=123&ol[0]=1&ol[1]=2&ul[]=str&ul[]=array&user.Name=rob
+       //
+       // Action:
+       //   Example.Action(id int, ol []int, ul []string, user User)
+       //
+       // Calls:
+       //   Bind(params, "id", int): 123
+       //   Bind(params, "ol", []int): {1, 2}
+       //   Bind(params, "ul", []string): {"str", "array"}
+       //   Bind(params, "user", User): User{Name:"rob"}
+       //
+       // Note that only exported struct fields may be bound.
+       Bind func(params *Params, name string, typ reflect.Type) reflect.Value
+
+       // Unbind serializes a given value to one or more URL parameters of the given
+       // name.
+       Unbind func(output map[string]string, name string, val interface{})
+}
+
+var binderLog = RevelLog.New("section", "binder")
+
+// ValueBinder is adapter for easily making one-key-value binders.
+func ValueBinder(f func(value string, typ reflect.Type) reflect.Value) func(*Params, string, reflect.Type) reflect.Value {
+       return func(params *Params, name string, typ reflect.Type) reflect.Value {
+               vals, ok := params.Values[name]
+               if !ok || len(vals) == 0 {
+                       return reflect.Zero(typ)
+               }
+               return f(vals[0], typ)
+       }
+}
+
+// Revel's default date and time constants
+const (
+       DefaultDateFormat     = "2006-01-02"
+       DefaultDateTimeFormat = "2006-01-02 15:04"
+)
+
+// Binders type and kind definition
+var (
+       // These are the lookups to find a Binder for any type of data.
+       // The most specific binder found will be used (Type before Kind)
+       TypeBinders = make(map[reflect.Type]Binder)
+       KindBinders = make(map[reflect.Kind]Binder)
+
+       // Applications can add custom time formats to this array, and they will be
+       // automatically attempted when binding a time.Time.
+       TimeFormats = []string{}
+
+       DateFormat     string
+       DateTimeFormat string
+       TimeZone       = time.UTC
+
+       IntBinder = Binder{
+               Bind: ValueBinder(func(val string, typ reflect.Type) reflect.Value {
+                       if len(val) == 0 {
+                               return reflect.Zero(typ)
+                       }
+                       intValue, err := strconv.ParseInt(val, 10, 64)
+                       if err != nil {
+                               binderLog.Warn("IntBinder Conversion Error", "error", err)
+                               return reflect.Zero(typ)
+                       }
+                       pValue := reflect.New(typ)
+                       pValue.Elem().SetInt(intValue)
+                       return pValue.Elem()
+               }),
+               Unbind: func(output map[string]string, key string, val interface{}) {
+                       output[key] = fmt.Sprintf("%d", val)
+               },
+       }
+
+       UintBinder = Binder{
+               Bind: ValueBinder(func(val string, typ reflect.Type) reflect.Value {
+                       if len(val) == 0 {
+                               return reflect.Zero(typ)
+                       }
+                       uintValue, err := strconv.ParseUint(val, 10, 64)
+                       if err != nil {
+                               binderLog.Warn("UintBinder Conversion Error", "error", err)
+                               return reflect.Zero(typ)
+                       }
+                       pValue := reflect.New(typ)
+                       pValue.Elem().SetUint(uintValue)
+                       return pValue.Elem()
+               }),
+               Unbind: func(output map[string]string, key string, val interface{}) {
+                       output[key] = fmt.Sprintf("%d", val)
+               },
+       }
+
+       FloatBinder = Binder{
+               Bind: ValueBinder(func(val string, typ reflect.Type) reflect.Value {
+                       if len(val) == 0 {
+                               return reflect.Zero(typ)
+                       }
+                       floatValue, err := strconv.ParseFloat(val, 64)
+                       if err != nil {
+                               binderLog.Warn("FloatBinder Conversion Error", "error", err)
+                               return reflect.Zero(typ)
+                       }
+                       pValue := reflect.New(typ)
+                       pValue.Elem().SetFloat(floatValue)
+                       return pValue.Elem()
+               }),
+               Unbind: func(output map[string]string, key string, val interface{}) {
+                       output[key] = fmt.Sprintf("%f", val)
+               },
+       }
+
+       StringBinder = Binder{
+               Bind: ValueBinder(func(val string, typ reflect.Type) reflect.Value {
+                       return reflect.ValueOf(val)
+               }),
+               Unbind: func(output map[string]string, name string, val interface{}) {
+                       output[name] = val.(string)
+               },
+       }
+
+       // Booleans support a various value formats,
+       // refer `revel.Atob` method.
+       BoolBinder = Binder{
+               Bind: ValueBinder(func(val string, typ reflect.Type) reflect.Value {
+                       return reflect.ValueOf(Atob(val))
+               }),
+               Unbind: func(output map[string]string, name string, val interface{}) {
+                       output[name] = fmt.Sprintf("%t", val)
+               },
+       }
+
+       PointerBinder = Binder{
+               Bind: func(params *Params, name string, typ reflect.Type) reflect.Value {
+                       v := Bind(params, name, typ.Elem())
+                       if v.CanAddr() {
+                               return v.Addr()
+                       }
+
+                       return v
+               },
+               Unbind: func(output map[string]string, name string, val interface{}) {
+                       Unbind(output, name, reflect.ValueOf(val).Elem().Interface())
+               },
+       }
+
+       TimeBinder = Binder{
+               Bind: ValueBinder(func(val string, typ reflect.Type) reflect.Value {
+                       for _, f := range TimeFormats {
+                               if r, err := time.ParseInLocation(f, val, TimeZone); err == nil {
+                                       return reflect.ValueOf(r)
+                               }
+                       }
+                       return reflect.Zero(typ)
+               }),
+               Unbind: func(output map[string]string, name string, val interface{}) {
+                       var (
+                               t       = val.(time.Time)
+                               format  = DateTimeFormat
+                               h, m, s = t.Clock()
+                       )
+                       if h == 0 && m == 0 && s == 0 {
+                               format = DateFormat
+                       }
+                       output[name] = t.Format(format)
+               },
+       }
+
+       MapBinder = Binder{
+               Bind:   bindMap,
+               Unbind: unbindMap,
+       }
+)
+
+// Used to keep track of the index for individual keyvalues.
+type sliceValue struct {
+       index int           // Index extracted from brackets.  If -1, no index was provided.
+       value reflect.Value // the bound value for this slice element.
+}
+
+// This function creates a slice of the given type, Binds each of the individual
+// elements, and then sets them to their appropriate location in the slice.
+// If elements are provided without an explicit index, they are added (in
+// unspecified order) to the end of the slice.
+func bindSlice(params *Params, name string, typ reflect.Type) reflect.Value {
+       // Collect an array of slice elements with their indexes (and the max index).
+       maxIndex := -1
+       numNoIndex := 0
+       sliceValues := []sliceValue{}
+
+       // Factor out the common slice logic (between form values and files).
+       processElement := func(key string, vals []string, files []*multipart.FileHeader) {
+               if !strings.HasPrefix(key, name+"[") {
+                       return
+               }
+
+               // Extract the index, and the index where a sub-key starts. (e.g. field[0].subkey)
+               index := -1
+               leftBracket, rightBracket := len(name), strings.Index(key[len(name):], "]")+len(name)
+               if rightBracket > leftBracket+1 {
+                       index, _ = strconv.Atoi(key[leftBracket+1 : rightBracket])
+               }
+               subKeyIndex := rightBracket + 1
+
+               // Handle the indexed case.
+               if index > -1 {
+                       if index > maxIndex {
+                               maxIndex = index
+                       }
+                       sliceValues = append(sliceValues, sliceValue{
+                               index: index,
+                               value: Bind(params, key[:subKeyIndex], typ.Elem()),
+                       })
+                       return
+               }
+
+               // It's an un-indexed element.  (e.g. element[])
+               numNoIndex += len(vals) + len(files)
+               for _, val := range vals {
+                       // Unindexed values can only be direct-bound.
+                       sliceValues = append(sliceValues, sliceValue{
+                               index: -1,
+                               value: BindValue(val, typ.Elem()),
+                       })
+               }
+
+               for _, fileHeader := range files {
+                       sliceValues = append(sliceValues, sliceValue{
+                               index: -1,
+                               value: BindFile(fileHeader, typ.Elem()),
+                       })
+               }
+       }
+
+       for key, vals := range params.Values {
+               processElement(key, vals, nil)
+       }
+       for key, fileHeaders := range params.Files {
+               processElement(key, nil, fileHeaders)
+       }
+
+       resultArray := reflect.MakeSlice(typ, maxIndex+1, maxIndex+1+numNoIndex)
+       for _, sv := range sliceValues {
+               if sv.index != -1 {
+                       resultArray.Index(sv.index).Set(sv.value)
+               } else {
+                       resultArray = reflect.Append(resultArray, sv.value)
+               }
+       }
+
+       return resultArray
+}
+
+// Break on dots and brackets.
+// e.g. bar => "bar", bar.baz => "bar", bar[0] => "bar"
+func nextKey(key string) string {
+       fieldLen := strings.IndexAny(key, ".[")
+       if fieldLen == -1 {
+               return key
+       }
+       return key[:fieldLen]
+}
+
+func unbindSlice(output map[string]string, name string, val interface{}) {
+       v := reflect.ValueOf(val)
+       for i := 0; i < v.Len(); i++ {
+               Unbind(output, fmt.Sprintf("%s[%d]", name, i), v.Index(i).Interface())
+       }
+}
+
+func bindStruct(params *Params, name string, typ reflect.Type) reflect.Value {
+       resultPointer := reflect.New(typ)
+       result := resultPointer.Elem()
+       if params.JSON != nil {
+               // Try to inject the response as a json into the created result
+               if err := json.Unmarshal(params.JSON, resultPointer.Interface()); err != nil {
+                       binderLog.Error("bindStruct Unable to unmarshal request", "name", name, "error", err, "data", string(params.JSON))
+               }
+               return result
+       }
+       fieldValues := make(map[string]reflect.Value)
+       for key := range params.Values {
+               if !strings.HasPrefix(key, name+".") {
+                       continue
+               }
+
+               // Get the name of the struct property.
+               // Strip off the prefix. e.g. foo.bar.baz => bar.baz
+               suffix := key[len(name)+1:]
+               fieldName := nextKey(suffix)
+               fieldLen := len(fieldName)
+
+               if _, ok := fieldValues[fieldName]; !ok {
+                       // Time to bind this field.  Get it and make sure we can set it.
+                       fieldValue := result.FieldByName(fieldName)
+                       if !fieldValue.IsValid() {
+                               binderLog.Warn("bindStruct Field not found", "name", fieldName)
+                               continue
+                       }
+                       if !fieldValue.CanSet() {
+                               binderLog.Warn("bindStruct Field not settable", "name", fieldName)
+                               continue
+                       }
+                       boundVal := Bind(params, key[:len(name)+1+fieldLen], fieldValue.Type())
+                       fieldValue.Set(boundVal)
+                       fieldValues[fieldName] = boundVal
+               }
+       }
+
+       return result
+}
+
+func unbindStruct(output map[string]string, name string, iface interface{}) {
+       val := reflect.ValueOf(iface)
+       typ := val.Type()
+       for i := 0; i < val.NumField(); i++ {
+               structField := typ.Field(i)
+               fieldValue := val.Field(i)
+
+               // PkgPath is specified to be empty exactly for exported fields.
+               if structField.PkgPath == "" {
+                       Unbind(output, fmt.Sprintf("%s.%s", name, structField.Name), fieldValue.Interface())
+               }
+       }
+}
+
+// Helper that returns an upload of the given name, or nil.
+func getMultipartFile(params *Params, name string) multipart.File {
+       for _, fileHeader := range params.Files[name] {
+               file, err := fileHeader.Open()
+               if err == nil {
+                       return file
+               }
+               binderLog.Warn("getMultipartFile: Failed to open uploaded file", "name", name, "error", err)
+       }
+       return nil
+}
+
+func bindFile(params *Params, name string, typ reflect.Type) reflect.Value {
+       reader := getMultipartFile(params, name)
+       if reader == nil {
+               return reflect.Zero(typ)
+       }
+
+       // If it's already stored in a temp file, just return that.
+       if osFile, ok := reader.(*os.File); ok {
+               return reflect.ValueOf(osFile)
+       }
+
+       // Otherwise, have to store it.
+       tmpFile, err := ioutil.TempFile("", "revel-upload")
+       if err != nil {
+               binderLog.Warn("bindFile: Failed to create a temp file to store upload", "name", name, "error", err)
+               return reflect.Zero(typ)
+       }
+
+       // Register it to be deleted after the request is done.
+       params.tmpFiles = append(params.tmpFiles, tmpFile)
+
+       _, err = io.Copy(tmpFile, reader)
+       if err != nil {
+               binderLog.Warn("bindFile: Failed to copy upload to temp file", "name", name, "error", err)
+               return reflect.Zero(typ)
+       }
+
+       _, err = tmpFile.Seek(0, 0)
+       if err != nil {
+               binderLog.Warn("bindFile: Failed to seek to beginning of temp file", "name", name, "error", err)
+               return reflect.Zero(typ)
+       }
+
+       return reflect.ValueOf(tmpFile)
+}
+
+func bindByteArray(params *Params, name string, typ reflect.Type) reflect.Value {
+       if reader := getMultipartFile(params, name); reader != nil {
+               b, err := ioutil.ReadAll(reader)
+               if err == nil {
+                       return reflect.ValueOf(b)
+               }
+               binderLog.Warn("bindByteArray: Error reading uploaded file contents", "name", name, "error", err)
+       }
+       return reflect.Zero(typ)
+}
+
+func bindReadSeeker(params *Params, name string, typ reflect.Type) reflect.Value {
+       if reader := getMultipartFile(params, name); reader != nil {
+               return reflect.ValueOf(reader.(io.ReadSeeker))
+       }
+       return reflect.Zero(typ)
+}
+
+// bindMap converts parameters using map syntax into the corresponding map. e.g.:
+//   params["a[5]"]=foo, name="a", typ=map[int]string => map[int]string{5: "foo"}
+func bindMap(params *Params, name string, typ reflect.Type) reflect.Value {
+       var (
+               keyType   = typ.Key()
+               valueType = typ.Elem()
+               resultPtr = reflect.New(reflect.MapOf(keyType, valueType))
+               result    = resultPtr.Elem()
+       )
+       result.Set(reflect.MakeMap(typ))
+       if params.JSON != nil {
+               // Try to inject the response as a json into the created result
+               if err := json.Unmarshal(params.JSON, resultPtr.Interface()); err != nil {
+                       binderLog.Warn("bindMap: Unable to unmarshal request", "name", name, "error", err)
+               }
+               return result
+       }
+
+       for paramName := range params.Values {
+               // The paramName string must start with the value in the "name" parameter,
+               // otherwise there is no way the parameter is part of the map
+               if !strings.HasPrefix(paramName, name) {
+                       continue
+               }
+
+               suffix := paramName[len(name)+1:]
+               fieldName := nextKey(suffix)
+               if fieldName != "" {
+                       fieldName = fieldName[:len(fieldName)-1]
+               }
+               if !strings.HasPrefix(paramName, name+"["+fieldName+"]") {
+                       continue
+               }
+
+               result.SetMapIndex(BindValue(fieldName, keyType), Bind(params, name+"["+fieldName+"]", valueType))
+       }
+       return result
+}
+
+func unbindMap(output map[string]string, name string, iface interface{}) {
+       mapValue := reflect.ValueOf(iface)
+       for _, key := range mapValue.MapKeys() {
+               Unbind(output, name+"["+fmt.Sprintf("%v", key.Interface())+"]",
+                       mapValue.MapIndex(key).Interface())
+       }
+}
+
+// Bind takes the name and type of the desired parameter and constructs it
+// from one or more values from Params.
+// Returns the zero value of the type upon any sort of failure.
+func Bind(params *Params, name string, typ reflect.Type) reflect.Value {
+       if binder, found := binderForType(typ); found {
+               return binder.Bind(params, name, typ)
+       }
+       return reflect.Zero(typ)
+}
+
+func BindValue(val string, typ reflect.Type) reflect.Value {
+       return Bind(&Params{Values: map[string][]string{"": {val}}}, "", typ)
+}
+
+func BindFile(fileHeader *multipart.FileHeader, typ reflect.Type) reflect.Value {
+       return Bind(&Params{Files: map[string][]*multipart.FileHeader{"": {fileHeader}}}, "", typ)
+}
+
+func Unbind(output map[string]string, name string, val interface{}) {
+       if binder, found := binderForType(reflect.TypeOf(val)); found {
+               if binder.Unbind != nil {
+                       binder.Unbind(output, name, val)
+               } else {
+                       binderLog.Error("Unbind: Unable to unmarshal request", "name", name, "value", val)
+               }
+       }
+}
+
+func binderForType(typ reflect.Type) (Binder, bool) {
+       binder, ok := TypeBinders[typ]
+       if !ok {
+               binder, ok = KindBinders[typ.Kind()]
+               if !ok {
+                       binderLog.Error("binderForType: no binder for type", "type", typ)
+                       return Binder{}, false
+               }
+       }
+       return binder, true
+}
+
+// Sadly, the binder lookups can not be declared initialized -- that results in
+// an "initialization loop" compile error.
+func init() {
+       KindBinders[reflect.Int] = IntBinder
+       KindBinders[reflect.Int8] = IntBinder
+       KindBinders[reflect.Int16] = IntBinder
+       KindBinders[reflect.Int32] = IntBinder
+       KindBinders[reflect.Int64] = IntBinder
+
+       KindBinders[reflect.Uint] = UintBinder
+       KindBinders[reflect.Uint8] = UintBinder
+       KindBinders[reflect.Uint16] = UintBinder
+       KindBinders[reflect.Uint32] = UintBinder
+       KindBinders[reflect.Uint64] = UintBinder
+
+       KindBinders[reflect.Float32] = FloatBinder
+       KindBinders[reflect.Float64] = FloatBinder
+
+       KindBinders[reflect.String] = StringBinder
+       KindBinders[reflect.Bool] = BoolBinder
+       KindBinders[reflect.Slice] = Binder{bindSlice, unbindSlice}
+       KindBinders[reflect.Struct] = Binder{bindStruct, unbindStruct}
+       KindBinders[reflect.Ptr] = PointerBinder
+       KindBinders[reflect.Map] = MapBinder
+
+       TypeBinders[reflect.TypeOf(time.Time{})] = TimeBinder
+
+       // Uploads
+       TypeBinders[reflect.TypeOf(&os.File{})] = Binder{bindFile, nil}
+       TypeBinders[reflect.TypeOf([]byte{})] = Binder{bindByteArray, nil}
+       TypeBinders[reflect.TypeOf((*io.Reader)(nil)).Elem()] = Binder{bindReadSeeker, nil}
+       TypeBinders[reflect.TypeOf((*io.ReadSeeker)(nil)).Elem()] = Binder{bindReadSeeker, nil}
+
+       OnAppStart(func() {
+               DateTimeFormat = Config.StringDefault("format.datetime", DefaultDateTimeFormat)
+               DateFormat = Config.StringDefault("format.date", DefaultDateFormat)
+               TimeFormats = append(TimeFormats, DateTimeFormat, DateFormat)
+       })
+}
diff --git a/src/foundation/api/revel/binder_test.go b/src/foundation/api/revel/binder_test.go
new file mode 100644 (file)
index 0000000..5ffd5cd
--- /dev/null
@@ -0,0 +1,419 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "encoding/json"
+       "fmt"
+       "io"
+       "io/ioutil"
+       "os"
+       "reflect"
+       "sort"
+       "strings"
+       "testing"
+       "time"
+)
+
+type A struct {
+       ID      int
+       Name    string
+       B       B
+       private int
+}
+
+type B struct {
+       Extra string
+}
+
+var (
+       ParamTestValues = map[string][]string{
+               "int":                            {"1"},
+               "int8":                           {"1"},
+               "int16":                          {"1"},
+               "int32":                          {"1"},
+               "int64":                          {"1"},
+               "uint":                           {"1"},
+               "uint8":                          {"1"},
+               "uint16":                         {"1"},
+               "uint32":                         {"1"},
+               "uint64":                         {"1"},
+               "float32":                        {"1.000000"},
+               "float64":                        {"1.000000"},
+               "str":                            {"hello"},
+               "bool-true":                      {"true"},
+               "bool-1":                         {"1"},
+               "bool-on":                        {"on"},
+               "bool-false":                     {"false"},
+               "bool-0":                         {"0"},
+               "bool-0.0":                       {"0.0"},
+               "bool-off":                       {"off"},
+               "bool-f":                         {"f"},
+               "date":                           {"1982-07-09"},
+               "datetime":                       {"1982-07-09 21:30"},
+               "customDate":                     {"07/09/1982"},
+               "arr[0]":                         {"1"},
+               "arr[1]":                         {"2"},
+               "arr[3]":                         {"3"},
+               "uarr[]":                         {"1", "2"},
+               "arruarr[0][]":                   {"1", "2"},
+               "arruarr[1][]":                   {"3", "4"},
+               "2darr[0][0]":                    {"0"},
+               "2darr[0][1]":                    {"1"},
+               "2darr[1][0]":                    {"10"},
+               "2darr[1][1]":                    {"11"},
+               "A.ID":                           {"123"},
+               "A.Name":                         {"rob"},
+               "B.ID":                           {"123"},
+               "B.Name":                         {"rob"},
+               "B.B.Extra":                      {"hello"},
+               "pB.ID":                          {"123"},
+               "pB.Name":                        {"rob"},
+               "pB.B.Extra":                     {"hello"},
+               "priv.private":                   {"123"},
+               "arrC[0].ID":                     {"5"},
+               "arrC[0].Name":                   {"rob"},
+               "arrC[0].B.Extra":                {"foo"},
+               "arrC[1].ID":                     {"8"},
+               "arrC[1].Name":                   {"bill"},
+               "m[a]":                           {"foo"},
+               "m[b]":                           {"bar"},
+               "m2[1]":                          {"foo"},
+               "m2[2]":                          {"bar"},
+               "m3[a]":                          {"1"},
+               "m3[b]":                          {"2"},
+               "m4[a].ID":                       {"1"},
+               "m4[a].Name":                     {"foo"},
+               "m4[b].ID":                       {"2"},
+               "m4[b].Name":                     {"bar"},
+               "mapWithAMuchLongerName[a].ID":   {"1"},
+               "mapWithAMuchLongerName[a].Name": {"foo"},
+               "mapWithAMuchLongerName[b].ID":   {"2"},
+               "mapWithAMuchLongerName[b].Name": {"bar"},
+               "invalidInt":                     {"xyz"},
+               "invalidInt2":                    {""},
+               "invalidBool":                    {"xyz"},
+               "invalidArr":                     {"xyz"},
+               "int8-overflow":                  {"1024"},
+               "uint8-overflow":                 {"1024"},
+       }
+
+       testDate     = time.Date(1982, time.July, 9, 0, 0, 0, 0, time.UTC)
+       testDatetime = time.Date(1982, time.July, 9, 21, 30, 0, 0, time.UTC)
+)
+
+var binderTestCases = map[string]interface{}{
+       "int":        1,
+       "int8":       int8(1),
+       "int16":      int16(1),
+       "int32":      int32(1),
+       "int64":      int64(1),
+       "uint":       1,
+       "uint8":      uint8(1),
+       "uint16":     uint16(1),
+       "uint32":     uint32(1),
+       "uint64":     uint64(1),
+       "float32":    float32(1.0),
+       "float64":    float64(1.0),
+       "str":        "hello",
+       "bool-true":  true,
+       "bool-1":     true,
+       "bool-on":    true,
+       "bool-false": false,
+       "bool-0":     false,
+       "bool-0.0":   false,
+       "bool-off":   false,
+       "bool-f":     false,
+       "date":       testDate,
+       "datetime":   testDatetime,
+       "customDate": testDate,
+       "arr":        []int{1, 2, 0, 3},
+       "uarr":       []int{1, 2},
+       "arruarr":    [][]int{{1, 2}, {3, 4}},
+       "2darr":      [][]int{{0, 1}, {10, 11}},
+       "A":          A{ID: 123, Name: "rob"},
+       "B":          A{ID: 123, Name: "rob", B: B{Extra: "hello"}},
+       "pB":         &A{ID: 123, Name: "rob", B: B{Extra: "hello"}},
+       "arrC": []A{
+               {
+                       ID:   5,
+                       Name: "rob",
+                       B:    B{"foo"},
+               },
+               {
+                       ID:   8,
+                       Name: "bill",
+               },
+       },
+       "m":  map[string]string{"a": "foo", "b": "bar"},
+       "m2": map[int]string{1: "foo", 2: "bar"},
+       "m3": map[string]int{"a": 1, "b": 2},
+       "m4": map[string]A{"a": {ID: 1, Name: "foo"}, "b": {ID: 2, Name: "bar"}},
+
+       // NOTE: We also include a map with a longer name than the others since this has caused problems
+       // described in github issue #1285, resolved in pull request #1344. This test case should
+       // prevent regression.
+       "mapWithAMuchLongerName": map[string]A{"a": {ID: 1, Name: "foo"}, "b": {ID: 2, Name: "bar"}},
+
+       // TODO: Tests that use TypeBinders
+
+       // Invalid value tests (the result should always be the zero value for that type)
+       // The point of these is to ensure that invalid user input does not cause panics.
+       "invalidInt":     0,
+       "invalidInt2":    0,
+       "invalidBool":    true,
+       "invalidArr":     []int{},
+       "priv":           A{},
+       "int8-overflow":  int8(0),
+       "uint8-overflow": uint8(0),
+}
+
+// Types that files may be bound to, and a func that can read the content from
+// that type.
+// TODO: Is there any way to create a slice, given only the element Type?
+var fileBindings = []struct{ val, arrval, f interface{} }{
+       {(**os.File)(nil), []*os.File{}, ioutil.ReadAll},
+       {(*[]byte)(nil), [][]byte{}, func(b []byte) []byte { return b }},
+       {(*io.Reader)(nil), []io.Reader{}, ioutil.ReadAll},
+       {(*io.ReadSeeker)(nil), []io.ReadSeeker{}, ioutil.ReadAll},
+}
+
+func TestJsonBinder(t *testing.T) {
+       // create a structure to be populated
+       {
+               d, _ := json.Marshal(map[string]int{"a": 1})
+               params := &Params{JSON: d}
+               foo := struct{ A int }{}
+               c := NewTestController(nil, getMultipartRequest())
+
+               ParseParams(params, NewRequest(c.Request.In))
+               actual := Bind(params, "test", reflect.TypeOf(foo))
+               valEq(t, "TestJsonBinder", reflect.ValueOf(actual.Interface().(struct{ A int }).A), reflect.ValueOf(1))
+       }
+       {
+               d, _ := json.Marshal(map[string]interface{}{"a": map[string]int{"b": 45}})
+               params := &Params{JSON: d}
+               testMap := map[string]interface{}{}
+               actual := Bind(params, "test", reflect.TypeOf(testMap)).Interface().(map[string]interface{})
+               if actual["a"].(map[string]interface{})["b"].(float64) != 45 {
+                       t.Errorf("Failed to fetch map value %#v", actual["a"])
+               }
+               // Check to see if a named map works
+               actualb := Bind(params, "test", reflect.TypeOf(map[string]map[string]float64{})).Interface().(map[string]map[string]float64)
+               if actualb["a"]["b"] != 45 {
+                       t.Errorf("Failed to fetch map value %#v", actual["a"])
+               }
+
+       }
+}
+
+func TestBinder(t *testing.T) {
+       // Reuse the mvc_test.go multipart request to test the binder.
+       params := &Params{}
+       c := NewTestController(nil, getMultipartRequest())
+       ParseParams(params, NewRequest(c.Request.In))
+       params.Values = ParamTestValues
+
+       // Values
+       for k, v := range binderTestCases {
+               actual := Bind(params, k, reflect.TypeOf(v))
+               expected := reflect.ValueOf(v)
+               valEq(t, k, actual, expected)
+       }
+
+       // Files
+
+       // Get the keys in sorted order to make the expectation right.
+       keys := []string{}
+       for k := range expectedFiles {
+               keys = append(keys, k)
+       }
+       sort.Strings(keys)
+
+       expectedBoundFiles := make(map[string][]fh)
+       for _, k := range keys {
+               fhs := expectedFiles[k]
+               k := nextKey(k)
+               expectedBoundFiles[k] = append(expectedBoundFiles[k], fhs...)
+       }
+
+       for k, fhs := range expectedBoundFiles {
+
+               if len(fhs) == 1 {
+                       // Test binding single files to: *os.File, []byte, io.Reader, io.ReadSeeker
+                       for _, binding := range fileBindings {
+                               typ := reflect.TypeOf(binding.val).Elem()
+                               actual := Bind(params, k, typ)
+                               if !actual.IsValid() || (actual.Kind() == reflect.Interface && actual.IsNil()) {
+                                       t.Errorf("%s (%s) - Returned nil.", k, typ)
+                                       continue
+                               }
+                               returns := reflect.ValueOf(binding.f).Call([]reflect.Value{actual})
+                               valEq(t, k, returns[0], reflect.ValueOf(fhs[0].content))
+                       }
+
+               } else {
+                       // Test binding multi to:
+                       // []*os.File, [][]byte, []io.Reader, []io.ReadSeeker
+                       for _, binding := range fileBindings {
+                               typ := reflect.TypeOf(binding.arrval)
+                               actual := Bind(params, k, typ)
+                               if actual.Len() != len(fhs) {
+                                       t.Fatalf("%s (%s) - Number of files: (expected) %d != %d (actual)",
+                                               k, typ, len(fhs), actual.Len())
+                               }
+                               for i := range fhs {
+                                       returns := reflect.ValueOf(binding.f).Call([]reflect.Value{actual.Index(i)})
+                                       if !returns[0].IsValid() {
+                                               t.Errorf("%s (%s) - Returned nil.", k, typ)
+                                               continue
+                                       }
+                                       valEq(t, k, returns[0], reflect.ValueOf(fhs[i].content))
+                               }
+                       }
+               }
+       }
+}
+
+// Unbinding tests
+
+var unbinderTestCases = map[string]interface{}{
+       "int":        1,
+       "int8":       int8(1),
+       "int16":      int16(1),
+       "int32":      int32(1),
+       "int64":      int64(1),
+       "uint":       1,
+       "uint8":      uint8(1),
+       "uint16":     uint16(1),
+       "uint32":     uint32(1),
+       "uint64":     uint64(1),
+       "float32":    float32(1.0),
+       "float64":    float64(1.0),
+       "str":        "hello",
+       "bool-true":  true,
+       "bool-false": false,
+       "date":       testDate,
+       "datetime":   testDatetime,
+       "arr":        []int{1, 2, 0, 3},
+       "2darr":      [][]int{{0, 1}, {10, 11}},
+       "A":          A{ID: 123, Name: "rob"},
+       "B":          A{ID: 123, Name: "rob", B: B{Extra: "hello"}},
+       "pB":         &A{ID: 123, Name: "rob", B: B{Extra: "hello"}},
+       "arrC": []A{
+               {
+                       ID:   5,
+                       Name: "rob",
+                       B:    B{"foo"},
+               },
+               {
+                       ID:   8,
+                       Name: "bill",
+               },
+       },
+       "m":  map[string]string{"a": "foo", "b": "bar"},
+       "m2": map[int]string{1: "foo", 2: "bar"},
+       "m3": map[string]int{"a": 1, "b": 2},
+}
+
+// Some of the unbinding results are not exactly what is in ParamTestValues, since it
+// serializes implicit zero values explicitly.
+var unbinderOverrideAnswers = map[string]map[string]string{
+       "arr": {
+               "arr[0]": "1",
+               "arr[1]": "2",
+               "arr[2]": "0",
+               "arr[3]": "3",
+       },
+       "A": {
+               "A.ID":      "123",
+               "A.Name":    "rob",
+               "A.B.Extra": "",
+       },
+       "arrC": {
+               "arrC[0].ID":      "5",
+               "arrC[0].Name":    "rob",
+               "arrC[0].B.Extra": "foo",
+               "arrC[1].ID":      "8",
+               "arrC[1].Name":    "bill",
+               "arrC[1].B.Extra": "",
+       },
+       "m":  {"m[a]": "foo", "m[b]": "bar"},
+       "m2": {"m2[1]": "foo", "m2[2]": "bar"},
+       "m3": {"m3[a]": "1", "m3[b]": "2"},
+}
+
+func TestUnbinder(t *testing.T) {
+       for k, v := range unbinderTestCases {
+               actual := make(map[string]string)
+               Unbind(actual, k, v)
+
+               // Get the expected key/values.
+               expected, ok := unbinderOverrideAnswers[k]
+               if !ok {
+                       expected = make(map[string]string)
+                       for k2, v2 := range ParamTestValues {
+                               if k == k2 || strings.HasPrefix(k2, k+".") || strings.HasPrefix(k2, k+"[") {
+                                       expected[k2] = v2[0]
+                               }
+                       }
+               }
+
+               // Compare length and values.
+               if len(actual) != len(expected) {
+                       t.Errorf("Length mismatch\nExpected length %d, actual %d\nExpected: %s\nActual: %s",
+                               len(expected), len(actual), expected, actual)
+               }
+               for k, v := range actual {
+                       if expected[k] != v {
+                               t.Errorf("Value mismatch.\nExpected: %s\nActual: %s", expected, actual)
+                       }
+               }
+       }
+}
+
+// Helpers
+
+func valEq(t *testing.T, name string, actual, expected reflect.Value) {
+       switch expected.Kind() {
+       case reflect.Slice:
+               // Check the type/length/element type
+               if !eq(t, name+" (type)", actual.Kind(), expected.Kind()) ||
+                       !eq(t, name+" (len)", actual.Len(), expected.Len()) ||
+                       !eq(t, name+" (elem)", actual.Type().Elem(), expected.Type().Elem()) {
+                       return
+               }
+
+               // Check value equality for each element.
+               for i := 0; i < actual.Len(); i++ {
+                       valEq(t, fmt.Sprintf("%s[%d]", name, i), actual.Index(i), expected.Index(i))
+               }
+
+       case reflect.Ptr:
+               // Check equality on the element type.
+               valEq(t, name, actual.Elem(), expected.Elem())
+       case reflect.Map:
+               if !eq(t, name+" (len)", actual.Len(), expected.Len()) {
+                       return
+               }
+               for _, key := range expected.MapKeys() {
+                       expectedValue := expected.MapIndex(key)
+                       actualValue := actual.MapIndex(key)
+                       if actualValue.IsValid() {
+                               valEq(t, fmt.Sprintf("%s[%s]", name, key), actualValue, expectedValue)
+                       } else {
+                               t.Errorf("Expected key %s not found", key)
+                       }
+               }
+       default:
+               eq(t, name, actual.Interface(), expected.Interface())
+       }
+}
+
+func init() {
+       DateFormat = DefaultDateFormat
+       DateTimeFormat = DefaultDateTimeFormat
+       TimeFormats = append(TimeFormats, DefaultDateFormat, DefaultDateTimeFormat, "01/02/2006")
+}
diff --git a/src/foundation/api/revel/cache/cache.go b/src/foundation/api/revel/cache/cache.go
new file mode 100644 (file)
index 0000000..7406004
--- /dev/null
@@ -0,0 +1,145 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package cache
+
+import (
+       "errors"
+       "time"
+)
+
+// Length of time to cache an item.
+const (
+       DefaultExpiryTime  = time.Duration(0)
+       ForEverNeverExpiry = time.Duration(-1)
+)
+
+// Getter is an interface for getting / decoding an element from a cache.
+type Getter interface {
+       // Get the content associated with the given key. decoding it into the given
+       // pointer.
+       //
+       // Returns:
+       //   - nil if the value was successfully retrieved and ptrValue set
+       //   - ErrCacheMiss if the value was not in the cache
+       //   - an implementation specific error otherwise
+       Get(key string, ptrValue interface{}) error
+}
+
+// Cache is an interface to an expiring cache.  It behaves (and is modeled) like
+// the Memcached interface.  It is keyed by strings (250 bytes at most).
+//
+// Many callers will make exclusive use of Set and Get, but more exotic
+// functions are also available.
+//
+// Example
+//
+// Here is a typical Get/Set interaction:
+//
+//   var items []*Item
+//   if err := cache.Get("items", &items); err != nil {
+//     items = loadItems()
+//     go cache.Set("items", items, cache.DefaultExpiryTime)
+//   }
+//
+// Note that the caller will frequently not wait for Set() to complete.
+//
+// Errors
+//
+// It is assumed that callers will infrequently check returned errors, since any
+// request should be fulfillable without finding anything in the cache.  As a
+// result, all errors other than ErrCacheMiss and ErrNotStored will be logged to
+// revel.ERROR, so that the developer does not need to check the return value to
+// discover things like deserialization or connection errors.
+type Cache interface {
+       // The Cache implements a Getter.
+       Getter
+
+       // Set the given key/value in the cache, overwriting any existing value
+       // associated with that key.  Keys may be at most 250 bytes in length.
+       //
+       // Returns:
+       //   - nil on success
+       //   - an implementation specific error otherwise
+       Set(key string, value interface{}, expires time.Duration) error
+
+       // Get the content associated multiple keys at once.  On success, the caller
+       // may decode the values one at a time from the returned Getter.
+       //
+       // Returns:
+       //   - the value getter, and a nil error if the operation completed.
+       //   - an implementation specific error otherwise
+       GetMulti(keys ...string) (Getter, error)
+
+       // Delete the given key from the cache.
+       //
+       // Returns:
+       //   - nil on a successful delete
+       //   - ErrCacheMiss if the value was not in the cache
+       //   - an implementation specific error otherwise
+       Delete(key string) error
+
+       // Add the given key/value to the cache ONLY IF the key does not already exist.
+       //
+       // Returns:
+       //   - nil if the value was added to the cache
+       //   - ErrNotStored if the key was already present in the cache
+       //   - an implementation-specific error otherwise
+       Add(key string, value interface{}, expires time.Duration) error
+
+       // Set the given key/value in the cache ONLY IF the key already exists.
+       //
+       // Returns:
+       //   - nil if the value was replaced
+       //   - ErrNotStored if the key does not exist in the cache
+       //   - an implementation specific error otherwise
+       Replace(key string, value interface{}, expires time.Duration) error
+
+       // Increment the value stored at the given key by the given amount.
+       // The value silently wraps around upon exceeding the uint64 range.
+       //
+       // Returns the new counter value if the operation was successful, or:
+       //   - ErrCacheMiss if the key was not found in the cache
+       //   - an implementation specific error otherwise
+       Increment(key string, n uint64) (newValue uint64, err error)
+
+       // Decrement the value stored at the given key by the given amount.
+       // The value is capped at 0 on underflow, with no error returned.
+       //
+       // Returns the new counter value if the operation was successful, or:
+       //   - ErrCacheMiss if the key was not found in the cache
+       //   - an implementation specific error otherwise
+       Decrement(key string, n uint64) (newValue uint64, err error)
+
+       // Expire all cache entries immediately.
+       // This is not implemented for the memcached cache (intentionally).
+       // Returns an implementation specific error if the operation failed.
+       Flush() error
+}
+
+var (
+       Instance Cache
+
+       ErrCacheMiss    = errors.New("revel/cache: key not found")
+       ErrNotStored    = errors.New("revel/cache: not stored")
+       ErrInvalidValue = errors.New("revel/cache: invalid value")
+)
+
+// The package implements the Cache interface (as sugar).
+
+func Get(key string, ptrValue interface{}) error                  { return Instance.Get(key, ptrValue) }
+func GetMulti(keys ...string) (Getter, error)                     { return Instance.GetMulti(keys...) }
+func Delete(key string) error                                     { return Instance.Delete(key) }
+func Increment(key string, n uint64) (newValue uint64, err error) { return Instance.Increment(key, n) }
+func Decrement(key string, n uint64) (newValue uint64, err error) { return Instance.Decrement(key, n) }
+func Flush() error                                                { return Instance.Flush() }
+func Set(key string, value interface{}, expires time.Duration) error {
+       return Instance.Set(key, value, expires)
+}
+func Add(key string, value interface{}, expires time.Duration) error {
+       return Instance.Add(key, value, expires)
+}
+func Replace(key string, value interface{}, expires time.Duration) error {
+       return Instance.Replace(key, value, expires)
+}
diff --git a/src/foundation/api/revel/cache/cache_test.go b/src/foundation/api/revel/cache/cache_test.go
new file mode 100644 (file)
index 0000000..bc54617
--- /dev/null
@@ -0,0 +1,253 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package cache
+
+import (
+       "math"
+       "testing"
+       "time"
+)
+
+// Tests against a generic Cache interface.
+// They should pass for all implementations.
+type cacheFactory func(*testing.T, time.Duration) Cache
+
+// Test typical cache interactions
+func typicalGetSet(t *testing.T, newCache cacheFactory) {
+       var err error
+       cache := newCache(t, time.Hour)
+
+       value := "foo"
+       if err = cache.Set("value", value, DefaultExpiryTime); err != nil {
+               t.Errorf("Error setting a value: %s", err)
+       }
+
+       value = ""
+       err = cache.Get("value", &value)
+       if err != nil {
+               t.Errorf("Error getting a value: %s", err)
+       }
+       if value != "foo" {
+               t.Errorf("Expected to get foo back, got %s", value)
+       }
+}
+
+// Test the increment-decrement cases
+func incrDecr(t *testing.T, newCache cacheFactory) {
+       var err error
+       cache := newCache(t, time.Hour)
+
+       // Normal increment / decrement operation.
+       if err = cache.Set("int", 10, ForEverNeverExpiry); err != nil {
+               t.Errorf("Error setting int: %s", err)
+       }
+       time.Sleep(time.Second)
+       newValue, err := cache.Increment("int", 50)
+       if err != nil {
+               t.Errorf("Error incrementing int: %s", err)
+       }
+       if newValue != 60 {
+               t.Errorf("Expected 60, was %d", newValue)
+       }
+
+       if newValue, err = cache.Decrement("int", 50); err != nil {
+               t.Errorf("Error decrementing: %s", err)
+       }
+       if newValue != 10 {
+               t.Errorf("Expected 10, was %d", newValue)
+       }
+
+       // Increment wraparound
+       newValue, err = cache.Increment("int", math.MaxUint64-5)
+       if err != nil {
+               t.Errorf("Error wrapping around: %s", err)
+       }
+       if newValue != 4 {
+               t.Errorf("Expected wraparound 4, got %d", newValue)
+       }
+
+       // Decrement capped at 0
+       newValue, err = cache.Decrement("int", 25)
+       if err != nil {
+               t.Errorf("Error decrementing below 0: %s", err)
+       }
+       if newValue != 0 {
+               t.Errorf("Expected capped at 0, got %d", newValue)
+       }
+}
+
+func expiration(t *testing.T, newCache cacheFactory) {
+       // memcached does not support expiration times less than 1 second.
+       var err error
+       cache := newCache(t, time.Second)
+       // Test Set w/ DefaultExpiryTime
+       value := 10
+       if err = cache.Set("int", value, DefaultExpiryTime); err != nil {
+               t.Errorf("Set failed: %s", err)
+       }
+       time.Sleep(2 * time.Second)
+       if err = cache.Get("int", &value); err != ErrCacheMiss {
+               t.Errorf("Expected CacheMiss, but got: %s", err)
+       }
+
+       // Test Set w/ short time
+       if err = cache.Set("int", value, time.Second); err != nil {
+               t.Errorf("Set failed: %s", err)
+       }
+       time.Sleep(2 * time.Second)
+       if err = cache.Get("int", &value); err != ErrCacheMiss {
+               t.Errorf("Expected CacheMiss, but got: %s", err)
+       }
+
+       // Test Set w/ longer time.
+       if err = cache.Set("int", value, time.Hour); err != nil {
+               t.Errorf("Set failed: %s", err)
+       }
+       time.Sleep(2 * time.Second)
+       if err = cache.Get("int", &value); err != nil {
+               t.Errorf("Expected to get the value, but got: %s", err)
+       }
+
+       // Test Set w/ forever.
+       if err = cache.Set("int", value, ForEverNeverExpiry); err != nil {
+               t.Errorf("Set failed: %s", err)
+       }
+       time.Sleep(2 * time.Second)
+       if err = cache.Get("int", &value); err != nil {
+               t.Errorf("Expected to get the value, but got: %s", err)
+       }
+}
+
+func emptyCache(t *testing.T, newCache cacheFactory) {
+       var err error
+       cache := newCache(t, time.Hour)
+
+       err = cache.Get("notexist", 0)
+       if err == nil {
+               t.Errorf("Error expected for non-existent key")
+       }
+       if err != ErrCacheMiss {
+               t.Errorf("Expected ErrCacheMiss for non-existent key: %s", err)
+       }
+
+       err = cache.Delete("notexist")
+       if err != ErrCacheMiss {
+               t.Errorf("Expected ErrCacheMiss for non-existent key: %s", err)
+       }
+
+       _, err = cache.Increment("notexist", 1)
+       if err != ErrCacheMiss {
+               t.Errorf("Expected cache miss incrementing non-existent key: %s", err)
+       }
+
+       _, err = cache.Decrement("notexist", 1)
+       if err != ErrCacheMiss {
+               t.Errorf("Expected cache miss decrementing non-existent key: %s", err)
+       }
+}
+
+func testReplace(t *testing.T, newCache cacheFactory) {
+       var err error
+       cache := newCache(t, time.Hour)
+
+       // Replace in an empty cache.
+       if err = cache.Replace("notexist", 1, ForEverNeverExpiry); err != ErrNotStored {
+               t.Errorf("Replace in empty cache: expected ErrNotStored, got: %s", err)
+       }
+
+       // Set a value of 1, and replace it with 2
+       if err = cache.Set("int", 1, time.Second); err != nil {
+               t.Errorf("Unexpected error: %s", err)
+       }
+
+       if err = cache.Replace("int", 2, time.Second); err != nil {
+               t.Errorf("Unexpected error: %s", err)
+       }
+       var i int
+       if err = cache.Get("int", &i); err != nil {
+               t.Errorf("Unexpected error getting a replaced item: %s", err)
+       }
+       if i != 2 {
+               t.Errorf("Expected 2, got %d", i)
+       }
+
+       // Wait for it to expire and replace with 3 (unsuccessfully).
+       time.Sleep(2 * time.Second)
+       if err = cache.Replace("int", 3, time.Second); err != ErrNotStored {
+               t.Errorf("Expected ErrNotStored, got: %s", err)
+       }
+       if err = cache.Get("int", &i); err != ErrCacheMiss {
+               t.Errorf("Expected cache miss, got: %s", err)
+       }
+}
+
+func testAdd(t *testing.T, newCache cacheFactory) {
+       var err error
+       cache := newCache(t, time.Hour)
+       // Add to an empty cache.
+       if err = cache.Add("int", 1, time.Second*3); err != nil {
+               t.Errorf("Unexpected error adding to empty cache: %s", err)
+       }
+
+       // Try to add again. (fail)
+       if err = cache.Add("int", 2, time.Second*3); err != nil {
+               if err != ErrNotStored {
+                       t.Errorf("Expected ErrNotStored adding dupe to cache: %s", err)
+               }
+       }
+
+       // Wait for it to expire, and add again.
+       time.Sleep(8 * time.Second)
+       if err = cache.Add("int", 3, time.Second*5); err != nil {
+               t.Errorf("Unexpected error adding to cache: %s", err)
+       }
+
+       // Get and verify the value.
+       var i int
+       if err = cache.Get("int", &i); err != nil {
+               t.Errorf("Unexpected error: %s", err)
+       }
+       if i != 3 {
+               t.Errorf("Expected 3, got: %d", i)
+       }
+}
+
+func testGetMulti(t *testing.T, newCache cacheFactory) {
+       cache := newCache(t, time.Hour)
+
+       m := map[string]interface{}{
+               "str": "foo",
+               "num": 42,
+               "foo": struct{ Bar string }{"baz"},
+       }
+
+       var keys []string
+       for key, value := range m {
+               keys = append(keys, key)
+               if err := cache.Set(key, value, time.Second*30); err != nil {
+                       t.Errorf("Error setting a value: %s", err)
+               }
+       }
+
+       g, err := cache.GetMulti(keys...)
+       if err != nil {
+               t.Errorf("Error in get-multi: %s", err)
+       }
+
+       var str string
+       if err = g.Get("str", &str); err != nil || str != "foo" {
+               t.Errorf("Error getting str: %s / %s", err, str)
+       }
+
+       var num int
+       if err = g.Get("num", &num); err != nil || num != 42 {
+               t.Errorf("Error getting num: %s / %v", err, num)
+       }
+
+       var foo struct{ Bar string }
+       if err = g.Get("foo", &foo); err != nil || foo.Bar != "baz" {
+               t.Errorf("Error getting foo: %s / %v", err, foo)
+       }
+}
diff --git a/src/foundation/api/revel/cache/init.go b/src/foundation/api/revel/cache/init.go
new file mode 100644 (file)
index 0000000..0008a6a
--- /dev/null
@@ -0,0 +1,60 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package cache
+
+import (
+       "strings"
+       "time"
+
+       "github.com/revel/revel"
+)
+
+var cacheLog = revel.RevelLog.New("section", "cache")
+
+func init() {
+       revel.OnAppStart(func() {
+               // Set the default expiration time.
+               defaultExpiration := time.Hour // The default for the default is one hour.
+               if expireStr, found := revel.Config.String("cache.expires"); found {
+                       var err error
+                       if defaultExpiration, err = time.ParseDuration(expireStr); err != nil {
+                               cacheLog.Panic("Could not parse default cache expiration duration " + expireStr + ": " + err.Error())
+                       }
+               }
+
+               // make sure you aren't trying to use both memcached and redis
+               if revel.Config.BoolDefault("cache.memcached", false) && revel.Config.BoolDefault("cache.redis", false) {
+                       cacheLog.Panic("You've configured both memcached and redis, please only include configuration for one cache!")
+               }
+
+               // Use memcached?
+               if revel.Config.BoolDefault("cache.memcached", false) {
+                       hosts := strings.Split(revel.Config.StringDefault("cache.hosts", ""), ",")
+                       if len(hosts) == 0 {
+                               cacheLog.Panic("Memcache enabled but no memcached hosts specified!")
+                       }
+
+                       Instance = NewMemcachedCache(hosts, defaultExpiration)
+                       return
+               }
+
+               // Use Redis (share same config as memcached)?
+               if revel.Config.BoolDefault("cache.redis", false) {
+                       hosts := strings.Split(revel.Config.StringDefault("cache.hosts", ""), ",")
+                       if len(hosts) == 0 {
+                               cacheLog.Panic("Redis enabled but no Redis hosts specified!")
+                       }
+                       if len(hosts) > 1 {
+                               cacheLog.Panic("Redis currently only supports one host!")
+                       }
+                       password := revel.Config.StringDefault("cache.redis.password", "")
+                       Instance = NewRedisCache(hosts[0], password, defaultExpiration)
+                       return
+               }
+
+               // By default, use the in-memory cache.
+               Instance = NewInMemoryCache(defaultExpiration)
+       })
+}
diff --git a/src/foundation/api/revel/cache/inmemory.go b/src/foundation/api/revel/cache/inmemory.go
new file mode 100644 (file)
index 0000000..8257f97
--- /dev/null
@@ -0,0 +1,163 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package cache
+
+import (
+       "fmt"
+       "reflect"
+       "time"
+
+       "github.com/patrickmn/go-cache"
+       "sync"
+)
+
+type InMemoryCache struct {
+       cache cache.Cache  // Only expose the methods we want to make available
+       mu    sync.RWMutex // For increment / decrement prevent reads and writes
+}
+
+func NewInMemoryCache(defaultExpiration time.Duration) InMemoryCache {
+       return InMemoryCache{cache: *cache.New(defaultExpiration, time.Minute), mu: sync.RWMutex{}}
+}
+
+func (c InMemoryCache) Get(key string, ptrValue interface{}) error {
+       c.mu.RLock()
+       defer c.mu.RUnlock()
+
+       value, found := c.cache.Get(key)
+       if !found {
+               return ErrCacheMiss
+       }
+
+       v := reflect.ValueOf(ptrValue)
+       if v.Type().Kind() == reflect.Ptr && v.Elem().CanSet() {
+               v.Elem().Set(reflect.ValueOf(value))
+               return nil
+       }
+
+       err := fmt.Errorf("revel/cache: attempt to get %s, but can not set value %v", key, v)
+       cacheLog.Error(err.Error())
+       return err
+}
+
+func (c InMemoryCache) GetMulti(keys ...string) (Getter, error) {
+       return c, nil
+}
+
+func (c InMemoryCache) Set(key string, value interface{}, expires time.Duration) error {
+       c.mu.Lock()
+       defer c.mu.Unlock()
+       // NOTE: go-cache understands the values of DefaultExpiryTime and ForEverNeverExpiry
+       c.cache.Set(key, value, expires)
+       return nil
+}
+
+func (c InMemoryCache) Add(key string, value interface{}, expires time.Duration) error {
+       c.mu.Lock()
+       defer c.mu.Unlock()
+       err := c.cache.Add(key, value, expires)
+       if err != nil {
+               return ErrNotStored
+       }
+       return err
+}
+
+func (c InMemoryCache) Replace(key string, value interface{}, expires time.Duration) error {
+       c.mu.Lock()
+       defer c.mu.Unlock()
+       if err := c.cache.Replace(key, value, expires); err != nil {
+               return ErrNotStored
+       }
+       return nil
+}
+
+func (c InMemoryCache) Delete(key string) error {
+       c.mu.RLock()
+       defer c.mu.RUnlock()
+       if _, found := c.cache.Get(key); !found {
+               return ErrCacheMiss
+       }
+       c.cache.Delete(key)
+       return nil
+}
+
+func (c InMemoryCache) Increment(key string, n uint64) (newValue uint64, err error) {
+       c.mu.Lock()
+       defer c.mu.Unlock()
+       if _, found := c.cache.Get(key); !found {
+               return 0, ErrCacheMiss
+       }
+       if err = c.cache.Increment(key, int64(n)); err != nil {
+               return
+       }
+
+       return c.convertTypeToUint64(key)
+}
+
+func (c InMemoryCache) Decrement(key string, n uint64) (newValue uint64, err error) {
+       c.mu.Lock()
+       defer c.mu.Unlock()
+       if nv, err := c.convertTypeToUint64(key); err != nil {
+               return 0, err
+       } else {
+               // Stop from going below zero
+               if n > nv {
+                       n = nv
+               }
+       }
+       if err = c.cache.Decrement(key, int64(n)); err != nil {
+               return
+       }
+
+       return c.convertTypeToUint64(key)
+}
+
+func (c InMemoryCache) Flush() error {
+       c.mu.Lock()
+       defer c.mu.Unlock()
+
+       c.cache.Flush()
+       return nil
+}
+
+// Fetches and returns the converted type to a uint64
+func (c InMemoryCache) convertTypeToUint64(key string) (newValue uint64, err error) {
+       v, found := c.cache.Get(key)
+       if !found {
+               return newValue, ErrCacheMiss
+       }
+
+       switch v.(type) {
+       case int:
+               newValue = uint64(v.(int))
+       case int8:
+               newValue = uint64(v.(int8))
+       case int16:
+               newValue = uint64(v.(int16))
+       case int32:
+               newValue = uint64(v.(int32))
+       case int64:
+               newValue = uint64(v.(int64))
+       case uint:
+               newValue = uint64(v.(uint))
+       case uintptr:
+               newValue = uint64(v.(uintptr))
+       case uint8:
+               newValue = uint64(v.(uint8))
+       case uint16:
+               newValue = uint64(v.(uint16))
+       case uint32:
+               newValue = uint64(v.(uint32))
+       case uint64:
+               newValue = uint64(v.(uint64))
+       case float32:
+               newValue = uint64(v.(float32))
+       case float64:
+               newValue = uint64(v.(float64))
+       default:
+               err = ErrInvalidValue
+       }
+       return
+}
diff --git a/src/foundation/api/revel/cache/inmemory_test.go b/src/foundation/api/revel/cache/inmemory_test.go
new file mode 100644 (file)
index 0000000..1f9cf1f
--- /dev/null
@@ -0,0 +1,44 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package cache
+
+import (
+       "testing"
+       "time"
+)
+
+var newInMemoryCache = func(_ *testing.T, defaultExpiration time.Duration) Cache {
+       return NewInMemoryCache(defaultExpiration)
+}
+
+// Test typical cache interactions
+func TestInMemoryCache_TypicalGetSet(t *testing.T) {
+       typicalGetSet(t, newInMemoryCache)
+}
+
+// Test the increment-decrement cases
+func TestInMemoryCache_IncrDecr(t *testing.T) {
+       incrDecr(t, newInMemoryCache)
+}
+
+func TestInMemoryCache_Expiration(t *testing.T) {
+       expiration(t, newInMemoryCache)
+}
+
+func TestInMemoryCache_EmptyCache(t *testing.T) {
+       emptyCache(t, newInMemoryCache)
+}
+
+func TestInMemoryCache_Replace(t *testing.T) {
+       testReplace(t, newInMemoryCache)
+}
+
+func TestInMemoryCache_Add(t *testing.T) {
+       testAdd(t, newInMemoryCache)
+}
+
+func TestInMemoryCache_GetMulti(t *testing.T) {
+       testGetMulti(t, newInMemoryCache)
+}
diff --git a/src/foundation/api/revel/cache/memcached.go b/src/foundation/api/revel/cache/memcached.go
new file mode 100644 (file)
index 0000000..fbc7ece
--- /dev/null
@@ -0,0 +1,118 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package cache
+
+import (
+       "errors"
+       "time"
+
+       "github.com/bradfitz/gomemcache/memcache"
+       "github.com/revel/revel/logger"
+)
+
+// MemcachedCache wraps the Memcached client to meet the Cache interface.
+type MemcachedCache struct {
+       *memcache.Client
+       defaultExpiration time.Duration
+}
+
+func NewMemcachedCache(hostList []string, defaultExpiration time.Duration) MemcachedCache {
+       return MemcachedCache{memcache.New(hostList...), defaultExpiration}
+}
+
+func (c MemcachedCache) Set(key string, value interface{}, expires time.Duration) error {
+       return c.invoke((*memcache.Client).Set, key, value, expires)
+}
+
+func (c MemcachedCache) Add(key string, value interface{}, expires time.Duration) error {
+       return c.invoke((*memcache.Client).Add, key, value, expires)
+}
+
+func (c MemcachedCache) Replace(key string, value interface{}, expires time.Duration) error {
+       return c.invoke((*memcache.Client).Replace, key, value, expires)
+}
+
+func (c MemcachedCache) Get(key string, ptrValue interface{}) error {
+       item, err := c.Client.Get(key)
+       if err != nil {
+               return convertMemcacheError(err)
+       }
+       return Deserialize(item.Value, ptrValue)
+}
+
+func (c MemcachedCache) GetMulti(keys ...string) (Getter, error) {
+       items, err := c.Client.GetMulti(keys)
+       if err != nil {
+               return nil, convertMemcacheError(err)
+       }
+       return ItemMapGetter(items), nil
+}
+
+func (c MemcachedCache) Delete(key string) error {
+       return convertMemcacheError(c.Client.Delete(key))
+}
+
+func (c MemcachedCache) Increment(key string, delta uint64) (newValue uint64, err error) {
+       newValue, err = c.Client.Increment(key, delta)
+       return newValue, convertMemcacheError(err)
+}
+
+func (c MemcachedCache) Decrement(key string, delta uint64) (newValue uint64, err error) {
+       newValue, err = c.Client.Decrement(key, delta)
+       return newValue, convertMemcacheError(err)
+}
+
+func (c MemcachedCache) Flush() error {
+       err := errors.New("Flush: can not flush memcached")
+       cacheLog.Error(err.Error())
+       return err
+}
+
+func (c MemcachedCache) invoke(f func(*memcache.Client, *memcache.Item) error,
+       key string, value interface{}, expires time.Duration) error {
+
+       switch expires {
+       case DefaultExpiryTime:
+               expires = c.defaultExpiration
+       case ForEverNeverExpiry:
+               expires = time.Duration(0)
+       }
+
+       b, err := Serialize(value)
+       if err != nil {
+               return err
+       }
+       return convertMemcacheError(f(c.Client, &memcache.Item{
+               Key:        key,
+               Value:      b,
+               Expiration: int32(expires / time.Second),
+       }))
+}
+
+// ItemMapGetter implements a Getter on top of the returned item map.
+type ItemMapGetter map[string]*memcache.Item
+
+func (g ItemMapGetter) Get(key string, ptrValue interface{}) error {
+       item, ok := g[key]
+       if !ok {
+               return ErrCacheMiss
+       }
+
+       return Deserialize(item.Value, ptrValue)
+}
+
+func convertMemcacheError(err error) error {
+       switch err {
+       case nil:
+               return nil
+       case memcache.ErrCacheMiss:
+               return ErrCacheMiss
+       case memcache.ErrNotStored:
+               return ErrNotStored
+       }
+
+       cacheLog.Error("convertMemcacheError:", "error", err, "trace", logger.NewCallStack())
+       return err
+}
diff --git a/src/foundation/api/revel/cache/memcached_test.go b/src/foundation/api/revel/cache/memcached_test.go
new file mode 100644 (file)
index 0000000..75376e0
--- /dev/null
@@ -0,0 +1,56 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package cache
+
+import (
+       "net"
+       "testing"
+       "time"
+)
+
+// These tests require memcached running on localhost:11211 (the default)
+const testServer = "localhost:11211"
+
+var newMemcachedCache = func(t *testing.T, defaultExpiration time.Duration) Cache {
+       c, err := net.Dial("tcp", testServer)
+       if err == nil {
+               if _, err = c.Write([]byte("flush_all\r\n")); err != nil {
+                       t.Errorf("Write failed: %s", err)
+               }
+               _ = c.Close()
+               return NewMemcachedCache([]string{testServer}, defaultExpiration)
+       }
+       t.Errorf("couldn't connect to memcached on %s", testServer)
+       t.FailNow()
+       panic("")
+}
+
+func TestMemcachedCache_TypicalGetSet(t *testing.T) {
+       typicalGetSet(t, newMemcachedCache)
+}
+
+func TestMemcachedCache_IncrDecr(t *testing.T) {
+       incrDecr(t, newMemcachedCache)
+}
+
+func TestMemcachedCache_Expiration(t *testing.T) {
+       expiration(t, newMemcachedCache)
+}
+
+func TestMemcachedCache_EmptyCache(t *testing.T) {
+       emptyCache(t, newMemcachedCache)
+}
+
+func TestMemcachedCache_Replace(t *testing.T) {
+       testReplace(t, newMemcachedCache)
+}
+
+func TestMemcachedCache_Add(t *testing.T) {
+       testAdd(t, newMemcachedCache)
+}
+
+func TestMemcachedCache_GetMulti(t *testing.T) {
+       testGetMulti(t, newMemcachedCache)
+}
diff --git a/src/foundation/api/revel/cache/redis.go b/src/foundation/api/revel/cache/redis.go
new file mode 100644 (file)
index 0000000..62aafc4
--- /dev/null
@@ -0,0 +1,273 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package cache
+
+import (
+       "time"
+
+       "github.com/garyburd/redigo/redis"
+       "github.com/revel/revel"
+)
+
+// RedisCache wraps the Redis client to meet the Cache interface.
+type RedisCache struct {
+       pool              *redis.Pool
+       defaultExpiration time.Duration
+}
+
+// NewRedisCache returns a new RedisCache with given parameters
+// until redigo supports sharding/clustering, only one host will be in hostList
+func NewRedisCache(host string, password string, defaultExpiration time.Duration) RedisCache {
+       var pool = &redis.Pool{
+               MaxIdle:     revel.Config.IntDefault("cache.redis.maxidle", 5),
+               MaxActive:   revel.Config.IntDefault("cache.redis.maxactive", 0),
+               IdleTimeout: time.Duration(revel.Config.IntDefault("cache.redis.idletimeout", 240)) * time.Second,
+               Dial: func() (redis.Conn, error) {
+                       protocol := revel.Config.StringDefault("cache.redis.protocol", "tcp")
+                       toc := time.Millisecond * time.Duration(revel.Config.IntDefault("cache.redis.timeout.connect", 10000))
+                       tor := time.Millisecond * time.Duration(revel.Config.IntDefault("cache.redis.timeout.read", 5000))
+                       tow := time.Millisecond * time.Duration(revel.Config.IntDefault("cache.redis.timeout.write", 5000))
+                       c, err := redis.Dial(protocol, host,
+                               redis.DialConnectTimeout(toc),
+                               redis.DialReadTimeout(tor),
+                               redis.DialWriteTimeout(tow))
+                       if err != nil {
+                               return nil, err
+                       }
+                       if len(password) > 0 {
+                               if _, err = c.Do("AUTH", password); err != nil {
+                                       _ = c.Close()
+                                       return nil, err
+                               }
+                       } else {
+                               // check with PING
+                               if _, err = c.Do("PING"); err != nil {
+                                       _ = c.Close()
+                                       return nil, err
+                               }
+                       }
+                       return c, err
+               },
+               // custom connection test method
+               TestOnBorrow: func(c redis.Conn, t time.Time) error {
+                       _, err := c.Do("PING")
+                       return err
+               },
+       }
+       return RedisCache{pool, defaultExpiration}
+}
+
+func (c RedisCache) Set(key string, value interface{}, expires time.Duration) error {
+       conn := c.pool.Get()
+       defer func() {
+               _ = conn.Close()
+       }()
+       return c.invoke(conn.Do, key, value, expires)
+}
+
+func (c RedisCache) Add(key string, value interface{}, expires time.Duration) error {
+       conn := c.pool.Get()
+       defer func() {
+               _ = conn.Close()
+       }()
+
+       existed, err := exists(conn, key)
+       if err != nil {
+               return err
+       } else if existed {
+               return ErrNotStored
+       }
+       return c.invoke(conn.Do, key, value, expires)
+}
+
+func (c RedisCache) Replace(key string, value interface{}, expires time.Duration) error {
+       conn := c.pool.Get()
+       defer func() {
+               _ = conn.Close()
+       }()
+
+       existed, err := exists(conn, key)
+       if err != nil {
+               return err
+       } else if !existed {
+               return ErrNotStored
+       }
+
+       err = c.invoke(conn.Do, key, value, expires)
+       if value == nil {
+               return ErrNotStored
+       }
+       return err
+}
+
+func (c RedisCache) Get(key string, ptrValue interface{}) error {
+       conn := c.pool.Get()
+       defer func() {
+               _ = conn.Close()
+       }()
+       raw, err := conn.Do("GET", key)
+       if err != nil {
+               return err
+       } else if raw == nil {
+               return ErrCacheMiss
+       }
+       item, err := redis.Bytes(raw, err)
+       if err != nil {
+               return err
+       }
+       return Deserialize(item, ptrValue)
+}
+
+func generalizeStringSlice(strs []string) []interface{} {
+       ret := make([]interface{}, len(strs))
+       for i, str := range strs {
+               ret[i] = str
+       }
+       return ret
+}
+
+func (c RedisCache) GetMulti(keys ...string) (Getter, error) {
+       conn := c.pool.Get()
+       defer func() {
+               _ = conn.Close()
+       }()
+
+       items, err := redis.Values(conn.Do("MGET", generalizeStringSlice(keys)...))
+       if err != nil {
+               return nil, err
+       } else if items == nil {
+               return nil, ErrCacheMiss
+       }
+
+       m := make(map[string][]byte)
+       for i, key := range keys {
+               m[key] = nil
+               if i < len(items) && items[i] != nil {
+                       s, ok := items[i].([]byte)
+                       if ok {
+                               m[key] = s
+                       }
+               }
+       }
+       return RedisItemMapGetter(m), nil
+}
+
+func exists(conn redis.Conn, key string) (bool, error) {
+       return redis.Bool(conn.Do("EXISTS", key))
+}
+
+func (c RedisCache) Delete(key string) error {
+       conn := c.pool.Get()
+       defer func() {
+               _ = conn.Close()
+       }()
+       existed, err := redis.Bool(conn.Do("DEL", key))
+       if err == nil && !existed {
+               err = ErrCacheMiss
+       }
+       return err
+}
+
+func (c RedisCache) Increment(key string, delta uint64) (uint64, error) {
+       conn := c.pool.Get()
+       defer func() {
+               _ = conn.Close()
+       }()
+       // Check for existence *before* increment as per the cache contract.
+       // redis will auto create the key, and we don't want that. Since we need to do increment
+       // ourselves instead of natively via INCRBY (redis doesn't support wrapping), we get the value
+       // and do the exists check this way to minimize calls to Redis
+       val, err := conn.Do("GET", key)
+       if err != nil {
+               return 0, err
+       } else if val == nil {
+               return 0, ErrCacheMiss
+       }
+       currentVal, err := redis.Int64(val, nil)
+       if err != nil {
+               return 0, err
+       }
+       sum := currentVal + int64(delta)
+       _, err = conn.Do("SET", key, sum)
+       if err != nil {
+               return 0, err
+       }
+       return uint64(sum), nil
+}
+
+func (c RedisCache) Decrement(key string, delta uint64) (newValue uint64, err error) {
+       conn := c.pool.Get()
+       defer func() {
+               _ = conn.Close()
+       }()
+       // Check for existence *before* increment as per the cache contract.
+       // redis will auto create the key, and we don't want that, hence the exists call
+       existed, err := exists(conn, key)
+       if err != nil {
+               return 0, err
+       } else if !existed {
+               return 0, ErrCacheMiss
+       }
+       // Decrement contract says you can only go to 0
+       // so we go fetch the value and if the delta is greater than the amount,
+       // 0 out the value
+       currentVal, err := redis.Int64(conn.Do("GET", key))
+       if err != nil {
+               return 0, err
+       }
+       if delta > uint64(currentVal) {
+               var tempint int64
+               tempint, err = redis.Int64(conn.Do("DECRBY", key, currentVal))
+               return uint64(tempint), err
+       }
+       tempint, err := redis.Int64(conn.Do("DECRBY", key, delta))
+       return uint64(tempint), err
+}
+
+func (c RedisCache) Flush() error {
+       conn := c.pool.Get()
+       defer func() {
+               _ = conn.Close()
+       }()
+       _, err := conn.Do("FLUSHALL")
+       return err
+}
+
+func (c RedisCache) invoke(f func(string, ...interface{}) (interface{}, error),
+       key string, value interface{}, expires time.Duration) error {
+
+       switch expires {
+       case DefaultExpiryTime:
+               expires = c.defaultExpiration
+       case ForEverNeverExpiry:
+               expires = time.Duration(0)
+       }
+
+       b, err := Serialize(value)
+       if err != nil {
+               return err
+       }
+       conn := c.pool.Get()
+       defer func() {
+               _ = conn.Close()
+       }()
+       if expires > 0 {
+               _, err = f("SETEX", key, int32(expires/time.Second), b)
+               return err
+       }
+       _, err = f("SET", key, b)
+       return err
+}
+
+// RedisItemMapGetter implements a Getter on top of the returned item map.
+type RedisItemMapGetter map[string][]byte
+
+func (g RedisItemMapGetter) Get(key string, ptrValue interface{}) error {
+       item, ok := g[key]
+       if !ok {
+               return ErrCacheMiss
+       }
+       return Deserialize(item, ptrValue)
+}
diff --git a/src/foundation/api/revel/cache/redis_test.go b/src/foundation/api/revel/cache/redis_test.go
new file mode 100644 (file)
index 0000000..ad0b006
--- /dev/null
@@ -0,0 +1,66 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package cache
+
+import (
+       "net"
+       "testing"
+       "time"
+
+       "github.com/revel/config"
+       "github.com/revel/revel"
+)
+
+// These tests require redis server running on localhost:6379 (the default)
+const redisTestServer = "localhost:6379"
+
+var newRedisCache = func(t *testing.T, defaultExpiration time.Duration) Cache {
+       revel.Config = config.NewContext()
+
+       c, err := net.Dial("tcp", redisTestServer)
+       if err == nil {
+               if _, err = c.Write([]byte("flush_all\r\n")); err != nil {
+                       t.Errorf("Write failed: %s", err)
+               }
+               _ = c.Close()
+
+               redisCache := NewRedisCache(redisTestServer, "", defaultExpiration)
+               if err = redisCache.Flush(); err != nil {
+                       t.Errorf("Flush failed: %s", err)
+               }
+               return redisCache
+       }
+       t.Errorf("couldn't connect to redis on %s", redisTestServer)
+       t.FailNow()
+       panic("")
+}
+
+func TestRedisCache_TypicalGetSet(t *testing.T) {
+       typicalGetSet(t, newRedisCache)
+}
+
+func TestRedisCache_IncrDecr(t *testing.T) {
+       incrDecr(t, newRedisCache)
+}
+
+func TestRedisCache_Expiration(t *testing.T) {
+       expiration(t, newRedisCache)
+}
+
+func TestRedisCache_EmptyCache(t *testing.T) {
+       emptyCache(t, newRedisCache)
+}
+
+func TestRedisCache_Replace(t *testing.T) {
+       testReplace(t, newRedisCache)
+}
+
+func TestRedisCache_Add(t *testing.T) {
+       testAdd(t, newRedisCache)
+}
+
+func TestRedisCache_GetMulti(t *testing.T) {
+       testGetMulti(t, newRedisCache)
+}
diff --git a/src/foundation/api/revel/cache/serialization.go b/src/foundation/api/revel/cache/serialization.go
new file mode 100644 (file)
index 0000000..de7ab65
--- /dev/null
@@ -0,0 +1,78 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package cache
+
+import (
+       "bytes"
+       "encoding/gob"
+       "reflect"
+       "strconv"
+)
+
+// Serialize transforms the given value into bytes following these rules:
+//   - If value is a byte array, it is returned as-is.
+//   - If value is an int or uint type, it is returned as the ASCII representation
+//   - Else, encoding/gob is used to serialize
+func Serialize(value interface{}) ([]byte, error) {
+       if data, ok := value.([]byte); ok {
+               return data, nil
+       }
+
+       switch v := reflect.ValueOf(value); v.Kind() {
+       case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+               return []byte(strconv.FormatInt(v.Int(), 10)), nil
+       case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+               return []byte(strconv.FormatUint(v.Uint(), 10)), nil
+       }
+
+       var b bytes.Buffer
+       encoder := gob.NewEncoder(&b)
+       if err := encoder.Encode(value); err != nil {
+               cacheLog.Error("Serialize: gob encoding failed", "value", value, "error", err)
+               return nil, err
+       }
+       return b.Bytes(), nil
+}
+
+// Deserialize transforms bytes produced by Serialize back into a Go object,
+// storing it into "ptr", which must be a pointer to the value type.
+func Deserialize(byt []byte, ptr interface{}) (err error) {
+       if data, ok := ptr.(*[]byte); ok {
+               *data = byt
+               return
+       }
+
+       if v := reflect.ValueOf(ptr); v.Kind() == reflect.Ptr {
+               switch p := v.Elem(); p.Kind() {
+               case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+                       var i int64
+                       i, err = strconv.ParseInt(string(byt), 10, 64)
+                       if err != nil {
+                               cacheLog.Error("Deserialize: failed to parse int", "value", string(byt), "error", err)
+                       } else {
+                               p.SetInt(i)
+                       }
+                       return
+
+               case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+                       var i uint64
+                       i, err = strconv.ParseUint(string(byt), 10, 64)
+                       if err != nil {
+                               cacheLog.Error("Deserialize: failed to parse uint", "value", string(byt), "error", err)
+                       } else {
+                               p.SetUint(i)
+                       }
+                       return
+               }
+       }
+
+       b := bytes.NewBuffer(byt)
+       decoder := gob.NewDecoder(b)
+       if err = decoder.Decode(ptr); err != nil {
+               cacheLog.Error("Deserialize: glob decoding failed", "error", err)
+               return
+       }
+       return
+}
diff --git a/src/foundation/api/revel/cache/serialization_test.go b/src/foundation/api/revel/cache/serialization_test.go
new file mode 100644 (file)
index 0000000..a2bb6e9
--- /dev/null
@@ -0,0 +1,87 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package cache
+
+import (
+       "reflect"
+       "testing"
+)
+
+type Struct1 struct {
+       X int
+}
+
+func (s Struct1) Method1() {}
+
+type Interface1 interface {
+       Method1()
+}
+
+var (
+       struct1                    = Struct1{1}
+       ptrStruct                  = &Struct1{2}
+       emptyIface     interface{} = Struct1{3}
+       iface1         Interface1  = Struct1{4}
+       sliceStruct                = []Struct1{{5}, {6}, {7}}
+       ptrSliceStruct             = []*Struct1{{8}, {9}, {10}}
+
+       valueMap = map[string]interface{}{
+               "bytes":          []byte{0x61, 0x62, 0x63, 0x64},
+               "string":         "string",
+               "bool":           true,
+               "int":            5,
+               "int8":           int8(5),
+               "int16":          int16(5),
+               "int32":          int32(5),
+               "int64":          int64(5),
+               "uint":           uint(5),
+               "uint8":          uint8(5),
+               "uint16":         uint16(5),
+               "uint32":         uint32(5),
+               "uint64":         uint64(5),
+               "float32":        float32(5),
+               "float64":        float64(5),
+               "array":          [5]int{1, 2, 3, 4, 5},
+               "slice":          []int{1, 2, 3, 4, 5},
+               "emptyIf":        emptyIface,
+               "Iface1":         iface1,
+               "map":            map[string]string{"foo": "bar"},
+               "ptrStruct":      ptrStruct,
+               "struct1":        struct1,
+               "sliceStruct":    sliceStruct,
+               "ptrSliceStruct": ptrSliceStruct,
+       }
+)
+
+// Test passing all kinds of data between serialize and deserialize.
+func TestRoundTrip(t *testing.T) {
+       for _, expected := range valueMap {
+               bytes, err := Serialize(expected)
+               if err != nil {
+                       t.Error(err)
+                       continue
+               }
+
+               ptrActual := reflect.New(reflect.TypeOf(expected)).Interface()
+               err = Deserialize(bytes, ptrActual)
+               if err != nil {
+                       t.Error(err)
+                       continue
+               }
+
+               actual := reflect.ValueOf(ptrActual).Elem().Interface()
+               if !reflect.DeepEqual(expected, actual) {
+                       t.Errorf("(expected) %T %v != %T %v (actual)", expected, expected, actual, actual)
+               }
+       }
+}
+
+func zeroMap(arg map[string]interface{}) map[string]interface{} {
+       result := map[string]interface{}{}
+       for key, value := range arg {
+               result[key] = reflect.Zero(reflect.TypeOf(value)).Interface()
+       }
+       return result
+}
diff --git a/src/foundation/api/revel/compress.go b/src/foundation/api/revel/compress.go
new file mode 100644 (file)
index 0000000..a44053d
--- /dev/null
@@ -0,0 +1,395 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "compress/gzip"
+       "compress/zlib"
+       "io"
+       "net/http"
+       "strconv"
+       "strings"
+)
+
+var compressionTypes = [...]string{
+       "gzip",
+       "deflate",
+}
+
+var compressableMimes = [...]string{
+       "text/plain",
+       "text/html",
+       "text/xml",
+       "text/css",
+       "application/json",
+       "application/xml",
+       "application/xhtml+xml",
+       "application/rss+xml",
+       "application/javascript",
+       "application/x-javascript",
+}
+
+// Local log instance for this class
+var compressLog = RevelLog.New("section", "compress")
+
+// WriteFlusher interface for compress writer
+type WriteFlusher interface {
+       io.Writer // An IO Writer
+       io.Closer // A closure
+       Flush() error /// A flush function
+}
+
+// The compressed writer
+type CompressResponseWriter struct {
+       Header             *BufferedServerHeader // The header
+       ControllerResponse *Response // The response
+       OriginalWriter     io.Writer // The writer
+       compressWriter     WriteFlusher // The flushed writer
+       compressionType    string // The compression type
+       headersWritten     bool // True if written
+       closeNotify        chan bool // The notify channel to close
+       parentNotify       <-chan bool // The parent chanel to receive the closed event
+       closed             bool // True if closed
+}
+
+// CompressFilter does compression of response body in gzip/deflate if
+// `results.compressed=true` in the app.conf
+func CompressFilter(c *Controller, fc []Filter) {
+       if c.Response.Out.internalHeader.Server != nil && Config.BoolDefault("results.compressed", false) {
+               if c.Response.Status != http.StatusNoContent && c.Response.Status != http.StatusNotModified {
+                       if found, compressType, compressWriter := detectCompressionType(c.Request, c.Response); found {
+                               writer := CompressResponseWriter{
+                                       ControllerResponse: c.Response,
+                                       OriginalWriter:     c.Response.GetWriter(),
+                                       compressWriter:     compressWriter,
+                                       compressionType:    compressType,
+                                       headersWritten:     false,
+                                       closeNotify:        make(chan bool, 1),
+                                       closed:             false,
+                               }
+                               // Swap out the header with our own
+                               writer.Header = NewBufferedServerHeader(c.Response.Out.internalHeader.Server)
+                               c.Response.Out.internalHeader.Server = writer.Header
+                               if w, ok := c.Response.GetWriter().(http.CloseNotifier); ok {
+                                       writer.parentNotify = w.CloseNotify()
+                               }
+                               c.Response.SetWriter(&writer)
+                       }
+               } else {
+                       compressLog.Debug("CompressFilter: Compression disabled for response ", "status", c.Response.Status)
+               }
+       }
+       fc[0](c, fc[1:])
+}
+
+// Called to notify the writer is closing
+func (c CompressResponseWriter) CloseNotify() <-chan bool {
+       if c.parentNotify != nil {
+               return c.parentNotify
+       }
+       return c.closeNotify
+}
+
+// Cancel the writer
+func (c *CompressResponseWriter) cancel() {
+       c.closed = true
+}
+
+// Prepare the headers
+func (c *CompressResponseWriter) prepareHeaders() {
+       if c.compressionType != "" {
+               responseMime := ""
+               if t := c.Header.Get("Content-Type"); len(t) > 0 {
+                       responseMime = t[0]
+               }
+               responseMime = strings.TrimSpace(strings.SplitN(responseMime, ";", 2)[0])
+               shouldEncode := false
+
+               if len(c.Header.Get("Content-Encoding")) == 0 {
+                       for _, compressableMime := range compressableMimes {
+                               if responseMime == compressableMime {
+                                       shouldEncode = true
+                                       c.Header.Set("Content-Encoding", c.compressionType)
+                                       c.Header.Del("Content-Length")
+                                       break
+                               }
+                       }
+               }
+
+               if !shouldEncode {
+                       c.compressWriter = nil
+                       c.compressionType = ""
+               }
+       }
+       c.Header.Release()
+}
+
+// Write the headers
+func (c *CompressResponseWriter) WriteHeader(status int) {
+       if c.closed {
+               return
+       }
+       c.headersWritten = true
+       c.prepareHeaders()
+       c.Header.SetStatus(status)
+}
+
+// Close the writer
+func (c *CompressResponseWriter) Close() error {
+       if c.closed {
+               return nil
+       }
+       if !c.headersWritten {
+               c.prepareHeaders()
+       }
+       if c.compressionType != "" {
+               c.Header.Del("Content-Length")
+               if err := c.compressWriter.Close(); err != nil {
+                       // TODO When writing directly to stream, an error will be generated
+                       compressLog.Error("Close: Error closing compress writer", "type", c.compressionType, "error", err)
+               }
+
+       }
+       // Non-blocking write to the closenotifier, if we for some reason should
+       // get called multiple times
+       select {
+       case c.closeNotify <- true:
+       default:
+       }
+       c.closed = true
+       return nil
+}
+
+// Write to the underling buffer
+func (c *CompressResponseWriter) Write(b []byte) (int, error) {
+       if c.closed {
+               return 0, io.ErrClosedPipe
+       }
+       // Abort if parent has been closed
+       if c.parentNotify != nil {
+               select {
+               case <-c.parentNotify:
+                       return 0, io.ErrClosedPipe
+               default:
+               }
+       }
+       // Abort if we ourselves have been closed
+       if c.closed {
+               return 0, io.ErrClosedPipe
+       }
+
+       if !c.headersWritten {
+               c.prepareHeaders()
+               c.headersWritten = true
+       }
+       if c.compressionType != "" {
+               return c.compressWriter.Write(b)
+       }
+       return c.OriginalWriter.Write(b)
+}
+
+// DetectCompressionType method detects the compression type
+// from header "Accept-Encoding"
+func detectCompressionType(req *Request, resp *Response) (found bool, compressionType string, compressionKind WriteFlusher) {
+       if Config.BoolDefault("results.compressed", false) {
+               acceptedEncodings := strings.Split(req.GetHttpHeader("Accept-Encoding"), ",")
+
+               largestQ := 0.0
+               chosenEncoding := len(compressionTypes)
+
+               // I have fixed one edge case for issue #914
+               // But it's better to cover all possible edge cases or
+               // Adapt to https://github.com/golang/gddo/blob/master/httputil/header/header.go#L172
+               for _, encoding := range acceptedEncodings {
+                       encoding = strings.TrimSpace(encoding)
+                       encodingParts := strings.SplitN(encoding, ";", 2)
+
+                       // If we are the format "gzip;q=0.8"
+                       if len(encodingParts) > 1 {
+                               q := strings.TrimSpace(encodingParts[1])
+                               if len(q) == 0 || !strings.HasPrefix(q, "q=") {
+                                       continue
+                               }
+
+                               // Strip off the q=
+                               num, err := strconv.ParseFloat(q[2:], 32)
+                               if err != nil {
+                                       continue
+                               }
+
+                               if num >= largestQ && num > 0 {
+                                       if encodingParts[0] == "*" {
+                                               chosenEncoding = 0
+                                               largestQ = num
+                                               continue
+                                       }
+                                       for i, encoding := range compressionTypes {
+                                               if encoding == encodingParts[0] {
+                                                       if i < chosenEncoding {
+                                                               largestQ = num
+                                                               chosenEncoding = i
+                                                       }
+                                                       break
+                                               }
+                                       }
+                               }
+                       } else {
+                               // If we can accept anything, chose our preferred method.
+                               if encodingParts[0] == "*" {
+                                       chosenEncoding = 0
+                                       largestQ = 1
+                                       break
+                               }
+                               // This is for just plain "gzip"
+                               for i, encoding := range compressionTypes {
+                                       if encoding == encodingParts[0] {
+                                               if i < chosenEncoding {
+                                                       largestQ = 1.0
+                                                       chosenEncoding = i
+                                               }
+                                               break
+                                       }
+                               }
+                       }
+               }
+
+               if largestQ == 0 {
+                       return
+               }
+
+               compressionType = compressionTypes[chosenEncoding]
+
+               switch compressionType {
+               case "gzip":
+                       compressionKind = gzip.NewWriter(resp.GetWriter())
+                       found = true
+               case "deflate":
+                       compressionKind = zlib.NewWriter(resp.GetWriter())
+                       found = true
+               }
+       }
+       return
+}
+
+// BufferedServerHeader will not send content out until the Released is called, from that point on it will act normally
+// It implements all the ServerHeader
+type BufferedServerHeader struct {
+       cookieList []string // The cookie list
+       headerMap  map[string][]string // The header map
+       status     int // The status
+       released   bool // True if released
+       original   ServerHeader // The original header
+}
+
+// Creates a new instance based on the ServerHeader
+func NewBufferedServerHeader(o ServerHeader) *BufferedServerHeader {
+       return &BufferedServerHeader{original: o, headerMap: map[string][]string{}}
+}
+
+// Sets the cookie
+func (bsh *BufferedServerHeader) SetCookie(cookie string) {
+       if bsh.released {
+               bsh.original.SetCookie(cookie)
+       } else {
+               bsh.cookieList = append(bsh.cookieList, cookie)
+       }
+}
+
+// Returns a cookie
+func (bsh *BufferedServerHeader) GetCookie(key string) (ServerCookie, error) {
+       return bsh.original.GetCookie(key)
+}
+
+// Sets (replace) the header key
+func (bsh *BufferedServerHeader) Set(key string, value string) {
+       if bsh.released {
+               bsh.original.Set(key, value)
+       } else {
+               bsh.headerMap[key] = []string{value}
+       }
+}
+
+// Add (append) to a key this value
+func (bsh *BufferedServerHeader) Add(key string, value string) {
+       if bsh.released {
+               bsh.original.Set(key, value)
+       } else {
+               old := []string{}
+               if v, found := bsh.headerMap[key]; found {
+                       old = v
+               }
+               bsh.headerMap[key] = append(old, value)
+       }
+}
+
+// Delete this key
+func (bsh *BufferedServerHeader) Del(key string) {
+       if bsh.released {
+               bsh.original.Del(key)
+       } else {
+               delete(bsh.headerMap, key)
+       }
+}
+
+// Get this key
+func (bsh *BufferedServerHeader) Get(key string) (value []string) {
+       if bsh.released {
+               value = bsh.original.Get(key)
+       } else {
+               if v, found := bsh.headerMap[key]; found && len(v) > 0 {
+                       value = v
+               } else {
+                       value = bsh.original.Get(key)
+               }
+       }
+       return
+}
+
+// Get all header keys
+func (bsh *BufferedServerHeader) GetKeys() (value []string) {
+       if bsh.released {
+               value = bsh.original.GetKeys()
+       } else {
+               value = bsh.original.GetKeys()
+               for key := range bsh.headerMap {
+                       found := false
+                       for _,v := range value {
+                               if v==key {
+                                       found = true
+                                       break
+                               }
+                       }
+                       if !found {
+                               value = append(value,key)
+                       }
+               }
+       }
+       return
+}
+
+// Set the status
+func (bsh *BufferedServerHeader) SetStatus(statusCode int) {
+       if bsh.released {
+               bsh.original.SetStatus(statusCode)
+       } else {
+               bsh.status = statusCode
+       }
+}
+
+// Release the header and push the results to the original
+func (bsh *BufferedServerHeader) Release() {
+       bsh.released = true
+       for k, v := range bsh.headerMap {
+               for _, r := range v {
+                       bsh.original.Set(k, r)
+               }
+       }
+       for _, c := range bsh.cookieList {
+               bsh.original.SetCookie(c)
+       }
+       if bsh.status > 0 {
+               bsh.original.SetStatus(bsh.status)
+       }
+}
diff --git a/src/foundation/api/revel/compress_test.go b/src/foundation/api/revel/compress_test.go
new file mode 100644 (file)
index 0000000..8b8b2fb
--- /dev/null
@@ -0,0 +1,61 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "net/http/httptest"
+       "strings"
+       "testing"
+)
+
+// Test that the render response is as expected.
+func TestBenchmarkCompressed(t *testing.T) {
+       startFakeBookingApp()
+       resp := httptest.NewRecorder()
+       c := NewTestController(resp, showRequest)
+       if err := c.SetAction("Hotels", "Show"); err != nil {
+               t.Errorf("SetAction failed: %s", err)
+       }
+       Config.SetOption("results.compressed", "true")
+       result := Hotels{c}.Show(3)
+       result.Apply(c.Request, c.Response)
+       if !strings.Contains(resp.Body.String(), "300 Main St.") {
+               t.Errorf("Failed to find hotel address in action response:\n%s", resp.Body)
+       }
+}
+
+func BenchmarkRenderCompressed(b *testing.B) {
+       startFakeBookingApp()
+       resp := httptest.NewRecorder()
+       resp.Body = nil
+       c := NewTestController(resp, showRequest)
+       if err := c.SetAction("Hotels", "Show"); err != nil {
+               b.Errorf("SetAction failed: %s", err)
+       }
+       Config.SetOption("results.compressed", "true")
+       b.ResetTimer()
+
+       hotels := Hotels{c}
+       for i := 0; i < b.N; i++ {
+               hotels.Show(3).Apply(c.Request, c.Response)
+       }
+}
+
+func BenchmarkRenderUnCompressed(b *testing.B) {
+       startFakeBookingApp()
+       resp := httptest.NewRecorder()
+       resp.Body = nil
+       c := NewTestController(resp, showRequest)
+       if err := c.SetAction("Hotels", "Show"); err != nil {
+               b.Errorf("SetAction failed: %s", err)
+       }
+       Config.SetOption("results.compressed", "false")
+       b.ResetTimer()
+
+       hotels := Hotels{c}
+       for i := 0; i < b.N; i++ {
+               hotels.Show(3).Apply(c.Request, c.Response)
+       }
+}
diff --git a/src/foundation/api/revel/conf/mime-types.conf b/src/foundation/api/revel/conf/mime-types.conf
new file mode 100644 (file)
index 0000000..43b0235
--- /dev/null
@@ -0,0 +1,545 @@
+3dm=x-world/x-3dmf
+3dmf=x-world/x-3dmf
+7z=application/x-7z-compressed
+a=application/octet-stream
+aab=application/x-authorware-bin
+aam=application/x-authorware-map
+aas=application/x-authorware-seg
+abc=text/vndabc
+ace=application/x-ace-compressed
+acgi=text/html
+afl=video/animaflex
+ai=application/postscript
+aif=audio/aiff
+aifc=audio/aiff
+aiff=audio/aiff
+aim=application/x-aim
+aip=text/x-audiosoft-intra
+alz=application/x-alz-compressed
+ani=application/x-navi-animation
+aos=application/x-nokia-9000-communicator-add-on-software
+aps=application/mime
+arc=application/x-arc-compressed
+arj=application/arj
+art=image/x-jg
+asf=video/x-ms-asf
+asm=text/x-asm
+asp=text/asp
+asx=application/x-mplayer2
+au=audio/basic
+avi=video/x-msvideo
+avs=video/avs-video
+bcpio=application/x-bcpio
+bin=application/mac-binary
+bmp=image/bmp
+boo=application/book
+book=application/book
+boz=application/x-bzip2
+bsh=application/x-bsh
+bz2=application/x-bzip2
+bz=application/x-bzip
+c++=text/plain
+c=text/x-c
+cab=application/vnd.ms-cab-compressed
+cat=application/vndms-pkiseccat
+cc=text/x-c
+ccad=application/clariscad
+cco=application/x-cocoa
+cdf=application/cdf
+cer=application/pkix-cert
+cha=application/x-chat
+chat=application/x-chat
+chrt=application/vnd.kde.kchart
+class=application/java
+# ? class=application/java-vm
+com=text/plain
+conf=text/plain
+cpio=application/x-cpio
+cpp=text/x-c
+cpt=application/mac-compactpro
+crl=application/pkcs-crl
+crt=application/pkix-cert
+crx=application/x-chrome-extension
+csh=text/x-scriptcsh
+css=text/css
+csv=text/csv
+cxx=text/plain
+dar=application/x-dar
+dcr=application/x-director
+deb=application/x-debian-package
+deepv=application/x-deepv
+def=text/plain
+der=application/x-x509-ca-cert
+dif=video/x-dv
+dir=application/x-director
+divx=video/divx
+dl=video/dl
+dmg=application/x-apple-diskimage
+doc=application/msword
+dot=application/msword
+dp=application/commonground
+drw=application/drafting
+dump=application/octet-stream
+dv=video/x-dv
+dvi=application/x-dvi
+dwf=drawing/x-dwf=(old)
+dwg=application/acad
+dxf=application/dxf
+dxr=application/x-director
+el=text/x-scriptelisp
+elc=application/x-bytecodeelisp=(compiled=elisp)
+eml=message/rfc822
+env=application/x-envoy
+eps=application/postscript
+es=application/x-esrehber
+etx=text/x-setext
+evy=application/envoy
+exe=application/octet-stream
+f77=text/x-fortran
+f90=text/x-fortran
+f=text/x-fortran
+fdf=application/vndfdf
+fif=application/fractals
+fli=video/fli
+flo=image/florian
+flv=video/x-flv
+flx=text/vndfmiflexstor
+fmf=video/x-atomic3d-feature
+for=text/x-fortran
+fpx=image/vndfpx
+frl=application/freeloader
+funk=audio/make
+g3=image/g3fax
+g=text/plain
+gif=image/gif
+gl=video/gl
+gsd=audio/x-gsm
+gsm=audio/x-gsm
+gsp=application/x-gsp
+gss=application/x-gss
+gtar=application/x-gtar
+gz=application/x-compressed
+gzip=application/x-gzip
+h=text/x-h
+hdf=application/x-hdf
+help=application/x-helpfile
+hgl=application/vndhp-hpgl
+hh=text/x-h
+hlb=text/x-script
+hlp=application/hlp
+hpg=application/vndhp-hpgl
+hpgl=application/vndhp-hpgl
+hqx=application/binhex
+hta=application/hta
+htc=text/x-component
+htm=text/html
+html=text/html
+htmls=text/html
+htt=text/webviewhtml
+htx=text/html
+ice=x-conference/x-cooltalk
+ico=image/x-icon
+ics=text/calendar
+icz=text/calendar
+idc=text/plain
+ief=image/ief
+iefs=image/ief
+iges=application/iges
+igs=application/iges
+ima=application/x-ima
+imap=application/x-httpd-imap
+inf=application/inf
+ins=application/x-internett-signup
+ip=application/x-ip2
+isu=video/x-isvideo
+it=audio/it
+iv=application/x-inventor
+ivr=i-world/i-vrml
+ivy=application/x-livescreen
+jam=audio/x-jam
+jav=text/x-java-source
+java=text/x-java-source
+jcm=application/x-java-commerce
+jfif-tbnl=image/jpeg
+jfif=image/jpeg
+jnlp=application/x-java-jnlp-file
+jpe=image/jpeg
+jpeg=image/jpeg
+jpg=image/jpeg
+jps=image/x-jps
+js=application/javascript
+json=application/json
+jut=image/jutvision
+kar=audio/midi
+karbon=application/vnd.kde.karbon
+kfo=application/vnd.kde.kformula
+flw=application/vnd.kde.kivio
+kml=application/vnd.google-earth.kml+xml
+kmz=application/vnd.google-earth.kmz
+kon=application/vnd.kde.kontour
+kpr=application/vnd.kde.kpresenter
+kpt=application/vnd.kde.kpresenter
+ksp=application/vnd.kde.kspread
+kwd=application/vnd.kde.kword
+kwt=application/vnd.kde.kword
+ksh=text/x-scriptksh
+la=audio/nspaudio
+lam=audio/x-liveaudio
+latex=application/x-latex
+lha=application/lha
+lhx=application/octet-stream
+list=text/plain
+lma=audio/nspaudio
+log=text/plain
+lsp=text/x-scriptlisp
+lst=text/plain
+lsx=text/x-la-asf
+ltx=application/x-latex
+lzh=application/octet-stream
+lzx=application/lzx
+m1v=video/mpeg
+m2a=audio/mpeg
+m2v=video/mpeg
+m3u=audio/x-mpegurl
+m=text/x-m
+man=application/x-troff-man
+manifest=text/cache-manifest
+map=application/x-navimap
+mar=text/plain
+mbd=application/mbedlet
+mc$=application/x-magic-cap-package-10
+mcd=application/mcad
+mcf=text/mcf
+mcp=application/netmc
+me=application/x-troff-me
+mht=message/rfc822
+mhtml=message/rfc822
+mid=application/x-midi
+midi=application/x-midi
+mif=application/x-frame
+mime=message/rfc822
+mjf=audio/x-vndaudioexplosionmjuicemediafile
+mjpg=video/x-motion-jpeg
+mm=application/base64
+mme=application/base64
+mod=audio/mod
+moov=video/quicktime
+mov=video/quicktime
+movie=video/x-sgi-movie
+mp2=audio/mpeg
+mp3=audio/mpeg3
+mp4=video/mp4
+mpa=audio/mpeg
+mpc=application/x-project
+mpe=video/mpeg
+mpeg=video/mpeg
+mpg=video/mpeg
+mpga=audio/mpeg
+mpp=application/vndms-project
+mpt=application/x-project
+mpv=application/x-project
+mpx=application/x-project
+mrc=application/marc
+ms=application/x-troff-ms
+mv=video/x-sgi-movie
+my=audio/make
+mzz=application/x-vndaudioexplosionmzz
+nap=image/naplps
+naplps=image/naplps
+nc=application/x-netcdf
+ncm=application/vndnokiaconfiguration-message
+nif=image/x-niff
+niff=image/x-niff
+nix=application/x-mix-transfer
+nsc=application/x-conference
+nvd=application/x-navidoc
+o=application/octet-stream
+oda=application/oda
+odb=application/vnd.oasis.opendocument.database
+odc=application/vnd.oasis.opendocument.chart
+odf=application/vnd.oasis.opendocument.formula
+odg=application/vnd.oasis.opendocument.graphics
+odi=application/vnd.oasis.opendocument.image
+odm=application/vnd.oasis.opendocument.text-master
+odp=application/vnd.oasis.opendocument.presentation
+ods=application/vnd.oasis.opendocument.spreadsheet
+odt=application/vnd.oasis.opendocument.text
+oga=audio/ogg
+ogg=audio/ogg
+ogv=video/ogg
+omc=application/x-omc
+omcd=application/x-omcdatamaker
+omcr=application/x-omcregerator
+otc=application/vnd.oasis.opendocument.chart-template
+otf=application/vnd.oasis.opendocument.formula-template
+otg=application/vnd.oasis.opendocument.graphics-template
+oth=application/vnd.oasis.opendocument.text-web
+oti=application/vnd.oasis.opendocument.image-template
+otm=application/vnd.oasis.opendocument.text-master
+otp=application/vnd.oasis.opendocument.presentation-template
+ots=application/vnd.oasis.opendocument.spreadsheet-template
+ott=application/vnd.oasis.opendocument.text-template
+p10=application/pkcs10
+p12=application/pkcs-12
+p7a=application/x-pkcs7-signature
+p7c=application/pkcs7-mime
+p7m=application/pkcs7-mime
+p7r=application/x-pkcs7-certreqresp
+p7s=application/pkcs7-signature
+p=text/x-pascal
+part=application/pro_eng
+pas=text/pascal
+pbm=image/x-portable-bitmap
+pcl=application/vndhp-pcl
+pct=image/x-pict
+pcx=image/x-pcx
+pdb=chemical/x-pdb
+pdf=application/pdf
+pfunk=audio/make
+pgm=image/x-portable-graymap
+pic=image/pict
+pict=image/pict
+pkg=application/x-newton-compatible-pkg
+pko=application/vndms-pkipko
+pl=text/x-scriptperl
+plx=application/x-pixclscript
+pm4=application/x-pagemaker
+pm5=application/x-pagemaker
+pm=text/x-scriptperl-module
+png=image/png
+pnm=application/x-portable-anymap
+pot=application/mspowerpoint
+pov=model/x-pov
+ppa=application/vndms-powerpoint
+ppm=image/x-portable-pixmap
+pps=application/mspowerpoint
+ppt=application/mspowerpoint
+ppz=application/mspowerpoint
+pre=application/x-freelance
+prt=application/pro_eng
+ps=application/postscript
+psd=application/octet-stream
+pvu=paleovu/x-pv
+pwz=application/vndms-powerpoint
+py=text/x-scriptphyton
+pyc=applicaiton/x-bytecodepython
+qcp=audio/vndqcelp
+qd3=x-world/x-3dmf
+qd3d=x-world/x-3dmf
+qif=image/x-quicktime
+qt=video/quicktime
+qtc=video/x-qtc
+qti=image/x-quicktime
+qtif=image/x-quicktime
+ra=audio/x-pn-realaudio
+ram=audio/x-pn-realaudio
+rar=application/x-rar-compressed
+ras=application/x-cmu-raster
+rast=image/cmu-raster
+rexx=text/x-scriptrexx
+rf=image/vndrn-realflash
+rgb=image/x-rgb
+rm=application/vndrn-realmedia
+rmi=audio/mid
+rmm=audio/x-pn-realaudio
+rmp=audio/x-pn-realaudio
+rng=application/ringing-tones
+rnx=application/vndrn-realplayer
+roff=application/x-troff
+rp=image/vndrn-realpix
+rpm=audio/x-pn-realaudio-plugin
+rt=text/vndrn-realtext
+rtf=text/richtext
+rtx=text/richtext
+rv=video/vndrn-realvideo
+s=text/x-asm
+s3m=audio/s3m
+s7z=application/x-7z-compressed
+saveme=application/octet-stream
+sbk=application/x-tbook
+scm=text/x-scriptscheme
+sdml=text/plain
+sdp=application/sdp
+sdr=application/sounder
+sea=application/sea
+set=application/set
+sgm=text/x-sgml
+sgml=text/x-sgml
+sh=text/x-scriptsh
+shar=application/x-bsh
+shtml=text/x-server-parsed-html
+sid=audio/x-psid
+skd=application/x-koan
+skm=application/x-koan
+skp=application/x-koan
+skt=application/x-koan
+sit=application/x-stuffit
+sitx=application/x-stuffitx
+sl=application/x-seelogo
+smi=application/smil
+smil=application/smil
+snd=audio/basic
+sol=application/solids
+spc=text/x-speech
+spl=application/futuresplash
+spr=application/x-sprite
+sprite=application/x-sprite
+spx=audio/ogg
+src=application/x-wais-source
+ssi=text/x-server-parsed-html
+ssm=application/streamingmedia
+sst=application/vndms-pkicertstore
+step=application/step
+stl=application/sla
+stp=application/step
+sv4cpio=application/x-sv4cpio
+sv4crc=application/x-sv4crc
+svf=image/vnddwg
+svg=image/svg+xml
+svr=application/x-world
+swf=application/x-shockwave-flash
+t=application/x-troff
+talk=text/x-speech
+tar=application/x-tar
+tbk=application/toolbook
+tcl=text/x-scripttcl
+tcsh=text/x-scripttcsh
+tex=application/x-tex
+texi=application/x-texinfo
+texinfo=application/x-texinfo
+text=text/plain
+tgz=application/gnutar
+tif=image/tiff
+tiff=image/tiff
+tr=application/x-troff
+tsi=audio/tsp-audio
+tsp=application/dsptype
+tsv=text/tab-separated-values
+turbot=image/florian
+txt=text/plain
+uil=text/x-uil
+uni=text/uri-list
+unis=text/uri-list
+unv=application/i-deas
+uri=text/uri-list
+uris=text/uri-list
+ustar=application/x-ustar
+uu=text/x-uuencode
+uue=text/x-uuencode
+vcd=application/x-cdlink
+vcf=text/x-vcard
+vcard=text/x-vcard
+vcs=text/x-vcalendar
+vda=application/vda
+vdo=video/vdo
+vew=application/groupwise
+viv=video/vivo
+vivo=video/vivo
+vmd=application/vocaltec-media-desc
+vmf=application/vocaltec-media-file
+voc=audio/voc
+vos=video/vosaic
+vox=audio/voxware
+vqe=audio/x-twinvq-plugin
+vqf=audio/x-twinvq
+vql=audio/x-twinvq-plugin
+vrml=application/x-vrml
+vrt=x-world/x-vrt
+vsd=application/x-visio
+vst=application/x-visio
+vsw=application/x-visio
+w60=application/wordperfect60
+w61=application/wordperfect61
+w6w=application/msword
+wav=audio/wav
+wb1=application/x-qpro
+wbmp=image/vnd.wap.wbmp
+web=application/vndxara
+wiz=application/msword
+wk1=application/x-123
+wmf=windows/metafile
+wml=text/vnd.wap.wml
+wmlc=application/vnd.wap.wmlc
+wmls=text/vnd.wap.wmlscript
+wmlsc=application/vnd.wap.wmlscriptc
+word=application/msword
+wp5=application/wordperfect
+wp6=application/wordperfect
+wp=application/wordperfect
+wpd=application/wordperfect
+wq1=application/x-lotus
+wri=application/mswrite
+wrl=application/x-world
+wrz=model/vrml
+wsc=text/scriplet
+wsrc=application/x-wais-source
+wtk=application/x-wintalk
+x-png=image/png
+xbm=image/x-xbitmap
+xdr=video/x-amt-demorun
+xgz=xgl/drawing
+xif=image/vndxiff
+xl=application/excel
+xla=application/excel
+xlb=application/excel
+xlc=application/excel
+xld=application/excel
+xlk=application/excel
+xll=application/excel
+xlm=application/excel
+xls=application/excel
+xlt=application/excel
+xlv=application/excel
+xlw=application/excel
+xm=audio/xm
+xml=text/xml
+xmz=xgl/movie
+xpix=application/x-vndls-xpix
+xpm=image/x-xpixmap
+xsr=video/x-amt-showrun
+xwd=image/x-xwd
+xyz=chemical/x-pdb
+z=application/x-compress
+zip=application/zip
+zoo=application/octet-stream
+zsh=text/x-scriptzsh
+# Office 2007 mess - http://wdg.uncc.edu/Microsoft_Office_2007_MIME_Types_for_Apache_and_IIS
+docx=application/vnd.openxmlformats-officedocument.wordprocessingml.document
+docm=application/vnd.ms-word.document.macroEnabled.12
+dotx=application/vnd.openxmlformats-officedocument.wordprocessingml.template
+dotm=application/vnd.ms-word.template.macroEnabled.12
+xlsx=application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
+xlsm=application/vnd.ms-excel.sheet.macroEnabled.12
+xltx=application/vnd.openxmlformats-officedocument.spreadsheetml.template
+xltm=application/vnd.ms-excel.template.macroEnabled.12
+xlsb=application/vnd.ms-excel.sheet.binary.macroEnabled.12
+xlam=application/vnd.ms-excel.addin.macroEnabled.12
+pptx=application/vnd.openxmlformats-officedocument.presentationml.presentation
+pptm=application/vnd.ms-powerpoint.presentation.macroEnabled.12
+ppsx=application/vnd.openxmlformats-officedocument.presentationml.slideshow
+ppsm=application/vnd.ms-powerpoint.slideshow.macroEnabled.12
+potx=application/vnd.openxmlformats-officedocument.presentationml.template
+potm=application/vnd.ms-powerpoint.template.macroEnabled.12
+ppam=application/vnd.ms-powerpoint.addin.macroEnabled.12
+sldx=application/vnd.openxmlformats-officedocument.presentationml.slide
+sldm=application/vnd.ms-powerpoint.slide.macroEnabled.12
+thmx=application/vnd.ms-officetheme
+onetoc=application/onenote
+onetoc2=application/onenote
+onetmp=application/onenote
+onepkg=application/onenote
+# koffice
+
+# iWork
+key=application/x-iwork-keynote-sffkey
+kth=application/x-iwork-keynote-sffkth
+nmbtemplate=application/x-iwork-numbers-sfftemplate
+numbers=application/x-iwork-numbers-sffnumbers
+pages=application/x-iwork-pages-sffpages
+template=application/x-iwork-pages-sfftemplate
+
+# Extensions for Mozilla apps (Firefox and friends)
+xpi=application/x-xpinstall
+
+# Opera extensions
+oex=application/x-opera-extension
diff --git a/src/foundation/api/revel/controller.go b/src/foundation/api/revel/controller.go
new file mode 100644 (file)
index 0000000..29dbf7c
--- /dev/null
@@ -0,0 +1,546 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "errors"
+       "fmt"
+       "io"
+       "net/http"
+       "os"
+       "path/filepath"
+       "reflect"
+       "runtime"
+       "strings"
+       "time"
+
+       "github.com/revel/revel/logger"
+       "github.com/revel/revel/session"
+       "github.com/revel/revel/utils"
+)
+
+// Controller Revel's controller structure that gets embedded in user defined
+// controllers
+type Controller struct {
+       Name          string          // The controller name, e.g. "Application"
+       Type          *ControllerType // A description of the controller type.
+       MethodName    string          // The method name, e.g. "Index"
+       MethodType    *MethodType     // A description of the invoked action type.
+       AppController interface{}     // The controller that was instantiated. embeds revel.Controller
+       Action        string          // The fully qualified action name, e.g. "App.Index"
+       ClientIP      string          // holds IP address of request came from
+
+       Request  *Request
+       Response *Response
+       Result   Result
+
+       Flash      Flash                  // User cookie, cleared after 1 request.
+       Session    session.Session        // Session, stored using the session engine specified
+       Params     *Params                // Parameters from URL and form (including multipart).
+       Args       map[string]interface{} // Per-request scratch space.
+       ViewArgs   map[string]interface{} // Variables passed to the template.
+       Validation *Validation            // Data validation helpers
+       Log        logger.MultiLogger     // Context Logger
+}
+
+// The map of controllers, controllers are mapped by using the namespace|controller_name as the key
+var controllers = make(map[string]*ControllerType)
+var controllerLog = RevelLog.New("section", "controller")
+
+// NewController returns new controller instance for Request and Response
+func NewControllerEmpty() *Controller {
+       return &Controller{Request: NewRequest(nil), Response: NewResponse(nil)}
+}
+
+// New controller, creates a new instance wrapping the request and response in it
+func NewController(context ServerContext) *Controller {
+       c := NewControllerEmpty()
+       c.SetController(context)
+       return c
+}
+
+// Sets the request and the response for the controller
+func (c *Controller) SetController(context ServerContext) {
+
+       c.Request.SetRequest(context.GetRequest())
+       c.Response.SetResponse(context.GetResponse())
+       c.Request.controller = c
+       c.Params = new(Params)
+       c.Args = map[string]interface{}{}
+       c.ViewArgs = map[string]interface{}{
+               "RunMode": RunMode,
+               "DevMode": DevMode,
+       }
+
+}
+func (c *Controller) Destroy() {
+       // When the instantiated controller gets injected
+       // It inherits this method, so we need to
+       // check to see if the controller is nil before performing
+       // any actions
+       if c == nil {
+               return
+       }
+       if c.AppController != nil {
+               c.resetAppControllerFields()
+               // Return this instance to the pool
+               appController := c.AppController
+               c.AppController = nil
+               if RevelConfig.Controller.Reuse {
+                       RevelConfig.Controller.CachedMap[c.Name].Push(appController)
+               }
+               c.AppController = nil
+       }
+
+       c.Request.Destroy()
+       c.Response.Destroy()
+       c.Params = nil
+       c.Args = nil
+       c.ViewArgs = nil
+       c.Name = ""
+       c.Type = nil
+       c.MethodName = ""
+       c.MethodType = nil
+       c.Action = ""
+       c.ClientIP = ""
+       c.Result = nil
+       c.Flash = Flash{}
+       c.Session = session.NewSession()
+       c.Params = nil
+       c.Validation = nil
+       c.Log = nil
+}
+
+// FlashParams serializes the contents of Controller.Params to the Flash
+// cookie.
+func (c *Controller) FlashParams() {
+       for key, vals := range c.Params.Values {
+               c.Flash.Out[key] = strings.Join(vals, ",")
+       }
+}
+
+func (c *Controller) SetCookie(cookie *http.Cookie) {
+       c.Response.Out.internalHeader.SetCookie(cookie.String())
+}
+
+type ErrorCoder interface {
+       HTTPCode() int
+}
+
+func (c *Controller) RenderError(err error) Result {
+       if coder, ok := err.(ErrorCoder); ok {
+               c.setStatusIfNil(coder.HTTPCode())
+       } else {
+               c.setStatusIfNil(http.StatusInternalServerError)
+       }
+
+       return ErrorResult{c.ViewArgs, err}
+}
+
+func (c *Controller) setStatusIfNil(status int) {
+       if c.Response.Status == 0 {
+               c.Response.Status = status
+       }
+}
+
+// Render a template corresponding to the calling Controller method.
+// Arguments will be added to c.ViewArgs prior to rendering the template.
+// They are keyed on their local identifier.
+//
+// For example:
+//
+//     func (c Users) ShowUser(id int) revel.Result {
+//              user := loadUser(id)
+//              return c.Render(user)
+//     }
+//
+// This action will render views/Users/ShowUser.html, passing in an extra
+// key-value "user": (User).
+//
+// This is the slower magical version which uses the runtime
+// to determine
+// 1) Set c.ViewArgs to the arguments passed into this function
+// 2) How to call the RenderTemplate by building the following line
+// c.RenderTemplate(c.Name + "/" + c.MethodType.Name + "." + c.Request.Format)
+//
+// If you want your code to run faster it is recommended you add the template values directly
+// to the c.ViewArgs and call c.RenderTemplate directly
+func (c *Controller) Render(extraViewArgs ...interface{}) Result {
+       c.setStatusIfNil(http.StatusOK)
+
+       // Get the calling function line number.
+       _, _, line, ok := runtime.Caller(1)
+       if !ok {
+               controllerLog.Error("Render: Failed to get Caller information")
+       }
+
+       // Get the extra ViewArgs passed in.
+       if renderArgNames, ok := c.MethodType.RenderArgNames[line]; ok {
+               if len(renderArgNames) == len(extraViewArgs) {
+                       for i, extraRenderArg := range extraViewArgs {
+                               c.ViewArgs[renderArgNames[i]] = extraRenderArg
+                       }
+               } else {
+                       controllerLog.Error(fmt.Sprint(len(renderArgNames), "RenderArg names found for",
+                               len(extraViewArgs), "extra ViewArgs"))
+               }
+       } else {
+               controllerLog.Error(fmt.Sprint("No RenderArg names found for Render call on line", line,
+                       "(Action", c.Action, ")"),"stack",logger.NewCallStack())
+       }
+
+       return c.RenderTemplate(c.Name + "/" + c.MethodType.Name + "." + c.Request.Format)
+}
+
+// RenderTemplate method does less magical way to render a template.
+// Renders the given template, using the current ViewArgs.
+func (c *Controller) RenderTemplate(templatePath string) Result {
+       c.setStatusIfNil(http.StatusOK)
+
+       // Get the Template.
+       lang, _ := c.ViewArgs[CurrentLocaleViewArg].(string)
+       template, err := MainTemplateLoader.TemplateLang(templatePath, lang)
+       if err != nil {
+               return c.RenderError(err)
+       }
+
+       return &RenderTemplateResult{
+               Template: template,
+               ViewArgs: c.ViewArgs,
+       }
+}
+
+// TemplateOutput returns the result of the template rendered using the controllers ViewArgs.
+func (c *Controller) TemplateOutput(templatePath string) (data []byte, err error) {
+       return TemplateOutputArgs(templatePath, c.ViewArgs)
+}
+
+// RenderJSON uses encoding/json.Marshal to return JSON to the client.
+func (c *Controller) RenderJSON(o interface{}) Result {
+       c.setStatusIfNil(http.StatusOK)
+
+       return RenderJSONResult{o, ""}
+}
+
+// RenderJSONP renders JSONP result using encoding/json.Marshal
+func (c *Controller) RenderJSONP(callback string, o interface{}) Result {
+       c.setStatusIfNil(http.StatusOK)
+
+       return RenderJSONResult{o, callback}
+}
+
+// RenderXML uses encoding/xml.Marshal to return XML to the client.
+func (c *Controller) RenderXML(o interface{}) Result {
+       c.setStatusIfNil(http.StatusOK)
+
+       return RenderXMLResult{o}
+}
+
+// RenderText renders plaintext in response, printf style.
+func (c *Controller) RenderText(text string, objs ...interface{}) Result {
+       c.setStatusIfNil(http.StatusOK)
+
+       finalText := text
+       if len(objs) > 0 {
+               finalText = fmt.Sprintf(text, objs...)
+       }
+       return &RenderTextResult{finalText}
+}
+
+// RenderHTML renders html in response
+func (c *Controller) RenderHTML(html string) Result {
+       c.setStatusIfNil(http.StatusOK)
+
+       return &RenderHTMLResult{html}
+}
+
+// Todo returns an HTTP 501 Not Implemented "todo" indicating that the
+// action isn't done yet.
+func (c *Controller) Todo() Result {
+       c.Response.Status = http.StatusNotImplemented
+       controllerLog.Debug("Todo: Not implemented function", "action", c.Action)
+       return c.RenderError(&Error{
+               Title:       "TODO",
+               Description: "This action is not implemented",
+       })
+}
+
+// NotFound returns an HTTP 404 Not Found response whose body is the
+// formatted string of msg and objs.
+func (c *Controller) NotFound(msg string, objs ...interface{}) Result {
+       finalText := msg
+       if len(objs) > 0 {
+               finalText = fmt.Sprintf(msg, objs...)
+       }
+       c.Response.Status = http.StatusNotFound
+       return c.RenderError(&Error{
+               Title:       "Not Found",
+               Description: finalText,
+       })
+}
+
+// Forbidden returns an HTTP 403 Forbidden response whose body is the
+// formatted string of msg and objs.
+func (c *Controller) Forbidden(msg string, objs ...interface{}) Result {
+       finalText := msg
+       if len(objs) > 0 {
+               finalText = fmt.Sprintf(msg, objs...)
+       }
+       c.Response.Status = http.StatusForbidden
+       return c.RenderError(&Error{
+               Title:       "Forbidden",
+               Description: finalText,
+       })
+}
+
+// RenderFileName returns a file indicated by the path as provided via the filename.
+// It can be either displayed inline or downloaded as an attachment.
+// The name and size are taken from the file info.
+func (c *Controller) RenderFileName(filename string, delivery ContentDisposition) Result {
+       f, err := os.Open(filename)
+       if err != nil {
+               c.Log.Errorf("Cant open file: %v", err)
+               return c.RenderError(err)
+       }
+       return c.RenderFile(f, delivery)
+}
+
+// RenderFile returns a file, either displayed inline or downloaded
+// as an attachment. The name and size are taken from the file info.
+func (c *Controller) RenderFile(file *os.File, delivery ContentDisposition) Result {
+       c.setStatusIfNil(http.StatusOK)
+
+       var (
+               modtime       = time.Now()
+               fileInfo, err = file.Stat()
+       )
+       if err != nil {
+               controllerLog.Error("RenderFile: error", "error", err)
+       }
+       if fileInfo != nil {
+               modtime = fileInfo.ModTime()
+       }
+       return c.RenderBinary(file, filepath.Base(file.Name()), delivery, modtime)
+}
+
+// RenderBinary is like RenderFile() except that it instead of a file on disk,
+// it renders data from memory (which could be a file that has not been written,
+// the output from some function, or bytes streamed from somewhere else, as long
+// it implements io.Reader).  When called directly on something generated or
+// streamed, modtime should mostly likely be time.Now().
+func (c *Controller) RenderBinary(memfile io.Reader, filename string, delivery ContentDisposition, modtime time.Time) Result {
+       c.setStatusIfNil(http.StatusOK)
+
+       return &BinaryResult{
+               Reader:   memfile,
+               Name:     filename,
+               Delivery: delivery,
+               Length:   -1, // http.ServeContent gets the length itself unless memfile is a stream.
+               ModTime:  modtime,
+       }
+}
+
+// Redirect to an action or to a URL.
+//   c.Redirect(Controller.Action)
+//   c.Redirect("/controller/action")
+//   c.Redirect("/controller/%d/action", id)
+func (c *Controller) Redirect(val interface{}, args ...interface{}) Result {
+       c.setStatusIfNil(http.StatusFound)
+
+       if url, ok := val.(string); ok {
+               if len(args) == 0 {
+                       return &RedirectToURLResult{url}
+               }
+               return &RedirectToURLResult{fmt.Sprintf(url, args...)}
+       }
+       return &RedirectToActionResult{val, args}
+}
+
+// This stats returns some interesting stats based on what is cached in memory
+// and what is available directly
+func (c *Controller) Stats() map[string]interface{} {
+       result := CurrentEngine.Stats()
+       if RevelConfig.Controller.Reuse {
+               result["revel-controllers"] = RevelConfig.Controller.Stack.String()
+               for key, appStack := range RevelConfig.Controller.CachedMap {
+                       result["app-" + key] = appStack.String()
+               }
+       }
+       return result
+}
+
+// Message performs a lookup for the given message name using the given
+// arguments using the current language defined for this controller.
+//
+// The current language is set by the i18n plugin.
+func (c *Controller) Message(message string, args ...interface{}) string {
+       return MessageFunc(c.Request.Locale, message, args...)
+}
+
+// SetAction sets the action that is being invoked in the current request.
+// It sets the following properties: Name, Action, Type, MethodType
+func (c *Controller) SetAction(controllerName, methodName string) error {
+
+       return c.SetTypeAction(controllerName, methodName, nil)
+}
+
+// SetAction sets the assigns the Controller type, sets the action and initializes the controller
+func (c *Controller) SetTypeAction(controllerName, methodName string, typeOfController *ControllerType) error {
+
+       // Look up the controller and method types.
+       if typeOfController == nil {
+               if c.Type = ControllerTypeByName(controllerName, anyModule); c.Type == nil {
+                       return errors.New("revel/controller: failed to find controller " + controllerName)
+               }
+       } else {
+               c.Type = typeOfController
+       }
+
+       // Note method name is case insensitive search
+       if c.MethodType = c.Type.Method(methodName); c.MethodType == nil {
+               return errors.New("revel/controller: failed to find action " + controllerName + "." + methodName)
+       }
+
+       c.Name, c.MethodName = c.Type.Type.Name(), c.MethodType.Name
+       c.Action = c.Name + "." + c.MethodName
+
+       // Update Logger with controller and namespace
+       if c.Log != nil {
+               c.Log = c.Log.New("action", c.Action, "namespace", c.Type.Namespace)
+       }
+
+       if RevelConfig.Controller.Reuse {
+               if _, ok := RevelConfig.Controller.CachedMap[c.Name]; !ok {
+                       // Create a new stack for this controller
+                       localType := c.Type.Type
+                       RevelConfig.Controller.CachedMap[c.Name] = utils.NewStackLock(
+                               RevelConfig.Controller.CachedStackSize,
+                               RevelConfig.Controller.CachedStackMaxSize,
+                               func() interface{} {
+                                       return reflect.New(localType).Interface()
+                               })
+               }
+               // Instantiate the controller.
+               c.AppController = RevelConfig.Controller.CachedMap[c.Name].Pop()
+       } else {
+               c.AppController = reflect.New(c.Type.Type).Interface()
+       }
+       c.setAppControllerFields()
+
+       return nil
+}
+
+func ControllerTypeByName(controllerName string, moduleSource *Module) (c *ControllerType) {
+       var found bool
+       if c, found = controllers[controllerName]; !found {
+               // Backup, passed in controllerName should be in lower case, but may not be
+               if c, found = controllers[strings.ToLower(controllerName)]; !found {
+                       controllerLog.Debug("ControllerTypeByName: Cannot find controller in controllers map ", "controller", controllerName)
+                       // Search for the controller by name
+                       for _, cType := range controllers {
+                               testControllerName := strings.ToLower(cType.Type.Name())
+                               if testControllerName == strings.ToLower(controllerName) && (cType.ModuleSource == moduleSource || moduleSource == anyModule) {
+                                       controllerLog.Warn("ControllerTypeByName: Matched empty namespace controller ", "controller", controllerName, "namespace", cType.ModuleSource.Name)
+                                       c = cType
+                                       found = true
+                                       break
+                               }
+                       }
+               }
+       }
+       return
+}
+
+// Injects this instance (c) into the AppController instance
+func (c *Controller) setAppControllerFields() {
+       appController := reflect.ValueOf(c.AppController).Elem()
+       cValue := reflect.ValueOf(c)
+       for _, index := range c.Type.ControllerIndexes {
+               appController.FieldByIndex(index).Set(cValue)
+       }
+}
+
+// Removes this instance (c) from the AppController instance
+func (c *Controller) resetAppControllerFields() {
+       appController := reflect.ValueOf(c.AppController).Elem()
+       // Zero out controller
+       for _, index := range c.Type.ControllerIndexes {
+               appController.FieldByIndex(index).Set(reflect.Zero(reflect.TypeOf(c.AppController).Elem().FieldByIndex(index).Type))
+       }
+}
+
+func findControllers(appControllerType reflect.Type) (indexes [][]int) {
+       // It might be a multi-level embedding. To find the controllers, we follow
+       // every anonymous field, using breadth-first search.
+       type nodeType struct {
+               val   reflect.Value
+               index []int
+       }
+       appControllerPtr := reflect.New(appControllerType)
+       queue := []nodeType{{appControllerPtr, []int{}}}
+       for len(queue) > 0 {
+               // Get the next value and de-reference it if necessary.
+               var (
+                       node     = queue[0]
+                       elem     = node.val
+                       elemType = elem.Type()
+               )
+               if elemType.Kind() == reflect.Ptr {
+                       elem = elem.Elem()
+                       elemType = elem.Type()
+               }
+               queue = queue[1:]
+
+               // #944 if the type's Kind is not `Struct` move on,
+               // otherwise `elem.NumField()` will panic
+               if elemType.Kind() != reflect.Struct {
+                       continue
+               }
+
+               // Look at all the struct fields.
+               for i := 0; i < elem.NumField(); i++ {
+                       // If this is not an anonymous field, skip it.
+                       structField := elemType.Field(i)
+                       if !structField.Anonymous {
+                               continue
+                       }
+
+                       fieldValue := elem.Field(i)
+                       fieldType := structField.Type
+
+                       // If it's a Controller, record the field indexes to get here.
+                       if fieldType == controllerPtrType {
+                               indexes = append(indexes, append(node.index, i))
+                               continue
+                       }
+
+                       queue = append(queue,
+                               nodeType{fieldValue, append(append([]int{}, node.index...), i)})
+               }
+       }
+       return
+}
+
+// RegisterController registers a Controller and its Methods with Revel.
+func RegisterController(c interface{}, methods []*MethodType) {
+       // De-star the controller type
+       // (e.g. given TypeOf((*Application)(nil)), want TypeOf(Application))
+       elem := reflect.TypeOf(c).Elem()
+
+       // De-star all of the method arg types too.
+       for _, m := range methods {
+               m.lowerName = strings.ToLower(m.Name)
+               for _, arg := range m.Args {
+                       arg.Type = arg.Type.Elem()
+               }
+       }
+
+       // Fetch module for controller, if none found controller must be part of the app
+       controllerModule := ModuleFromPath(elem.PkgPath(), true)
+
+       controllerType := AddControllerType(controllerModule, elem, methods)
+
+       controllerLog.Debug("RegisterController:Registered controller", "controller", controllerType.Name())
+}
diff --git a/src/foundation/api/revel/controller_type.go b/src/foundation/api/revel/controller_type.go
new file mode 100644 (file)
index 0000000..4ba2f33
--- /dev/null
@@ -0,0 +1,164 @@
+package revel
+
+import (
+       "reflect"
+       "strings"
+)
+
+// Controller registry and types.
+type ControllerType struct {
+       Namespace         string  // The namespace of the controller
+       ModuleSource      *Module // The module for the controller
+       Type              reflect.Type
+       Methods           []*MethodType
+       ControllerIndexes [][]int // FieldByIndex to all embedded *Controllers
+       ControllerEvents  *ControllerTypeEvents
+}
+type ControllerTypeEvents struct {
+       Before, After, Finally, Panic []*ControllerFieldPath
+}
+
+// The controller field path provides the caller the ability to invoke the call
+// directly
+type ControllerFieldPath struct {
+       IsPointer      bool
+       FieldIndexPath []int
+       FunctionCall   reflect.Value
+}
+
+type MethodType struct {
+       Name           string
+       Args           []*MethodArg
+       RenderArgNames map[int][]string
+       lowerName      string
+       Index          int
+}
+
+type MethodArg struct {
+       Name string
+       Type reflect.Type
+}
+
+// Adds the controller to the controllers map using its namespace, also adds it to the module list of controllers.
+// If the controller is in the main application it is added without its namespace as well.
+func AddControllerType(moduleSource *Module, controllerType reflect.Type, methods []*MethodType) (newControllerType *ControllerType) {
+       if moduleSource == nil {
+               moduleSource = appModule
+       }
+
+       newControllerType = &ControllerType{ModuleSource: moduleSource, Type: controllerType, Methods: methods, ControllerIndexes: findControllers(controllerType)}
+       newControllerType.ControllerEvents = NewControllerTypeEvents(newControllerType)
+       newControllerType.Namespace = moduleSource.Namespace()
+       controllerName := newControllerType.Name()
+
+       // Store the first controller only in the controllers map with the unmapped namespace.
+       if _, found := controllers[controllerName]; !found {
+               controllers[controllerName] = newControllerType
+               newControllerType.ModuleSource.AddController(newControllerType)
+               if newControllerType.ModuleSource == appModule {
+                       // Add the controller mapping into the global namespace
+                       controllers[newControllerType.ShortName()] = newControllerType
+               }
+       } else {
+               controllerLog.Errorf("Error, attempt to register duplicate controller as %s", controllerName)
+       }
+       controllerLog.Debugf("Registered controller: %s", controllerName)
+
+       return
+}
+
+// Method searches for a given exported method (case insensitive)
+func (ct *ControllerType) Method(name string) *MethodType {
+       lowerName := strings.ToLower(name)
+       for _, method := range ct.Methods {
+               if method.lowerName == lowerName {
+                       return method
+               }
+       }
+       return nil
+}
+
+// The controller name with the namespace
+func (ct *ControllerType) Name() string {
+       return ct.Namespace + ct.ShortName()
+}
+
+// The controller name without the namespace
+func (ct *ControllerType) ShortName() string {
+       return strings.ToLower(ct.Type.Name())
+}
+
+func NewControllerTypeEvents(c *ControllerType) (ce *ControllerTypeEvents) {
+       ce = &ControllerTypeEvents{}
+       // Parse the methods for the controller type, assign any control methods
+       checkType := c.Type
+       ce.check(checkType, []int{})
+       return
+}
+
+// Add in before after panic and finally, recursive call
+// Befores are ordered in revers, everything else is in order of first encountered
+func (cte *ControllerTypeEvents) check(theType reflect.Type, fieldPath []int) {
+       typeChecker := func(checkType reflect.Type) {
+               for index := 0; index < checkType.NumMethod(); index++ {
+                       m := checkType.Method(index)
+                       // Must be two arguments, the second returns the controller type
+                       // Go cannot differentiate between promoted methods and
+                       // embedded methods, this allows the embedded method to be run
+                       // https://github.com/golang/go/issues/21162
+                       if m.Type.NumOut() == 2 && m.Type.Out(1) == checkType {
+                               if checkType.Kind() == reflect.Ptr {
+                                       controllerLog.Debug("Found controller type event method pointer", "name", checkType.Elem().Name(), "methodname", m.Name)
+                               } else {
+                                       controllerLog.Debug("Found controller type event method", "name", checkType.Name(), "methodname", m.Name)
+                               }
+                               controllerFieldPath := newFieldPath(checkType.Kind() == reflect.Ptr, m.Func, fieldPath)
+                               switch strings.ToLower(m.Name) {
+                               case "before":
+                                       cte.Before = append([]*ControllerFieldPath{controllerFieldPath}, cte.Before...)
+                               case "after":
+                                       cte.After = append(cte.After, controllerFieldPath)
+                               case "panic":
+                                       cte.Panic = append(cte.Panic, controllerFieldPath)
+                               case "finally":
+                                       cte.Finally = append(cte.Finally, controllerFieldPath)
+                               }
+                       }
+               }
+       }
+
+       // Check methods of both types
+       typeChecker(theType)
+       typeChecker(reflect.PtrTo(theType))
+
+       // Check for any sub controllers, ignore any pointers to controllers revel.Controller
+       for i := 0; i < theType.NumField(); i++ {
+               v := theType.Field(i)
+
+               switch v.Type.Kind() {
+               case reflect.Struct:
+                       cte.check(v.Type, append(fieldPath, i))
+               }
+       }
+}
+func newFieldPath(isPointer bool, value reflect.Value, fieldPath []int) *ControllerFieldPath {
+       return &ControllerFieldPath{IsPointer: isPointer, FunctionCall: value, FieldIndexPath: fieldPath}
+}
+
+func (fieldPath *ControllerFieldPath) Invoke(value reflect.Value, input []reflect.Value) (result []reflect.Value) {
+       for _, index := range fieldPath.FieldIndexPath {
+               // You can only fetch fields from non pointers
+               if value.Type().Kind() == reflect.Ptr {
+                       value = value.Elem().Field(index)
+               } else {
+                       value = value.Field(index)
+               }
+       }
+       if fieldPath.IsPointer && value.Type().Kind() != reflect.Ptr {
+               value = value.Addr()
+       } else if !fieldPath.IsPointer && value.Type().Kind() == reflect.Ptr {
+               value = value.Elem()
+       }
+
+       return fieldPath.FunctionCall.Call(append([]reflect.Value{value}, input...))
+}
diff --git a/src/foundation/api/revel/errors.go b/src/foundation/api/revel/errors.go
new file mode 100644 (file)
index 0000000..dc3807a
--- /dev/null
@@ -0,0 +1,155 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "fmt"
+       "path/filepath"
+       "runtime/debug"
+       "strconv"
+       "strings"
+)
+
+// Error description, used as an argument to the error template.
+type Error struct {
+       SourceType               string   // The type of source that failed to build.
+       Title, Path, Description string   // Description of the error, as presented to the user.
+       Line, Column             int      // Where the error was encountered.
+       SourceLines              []string // The entire source file, split into lines.
+       Stack                    string   // The raw stack trace string from debug.Stack().
+       MetaError                string   // Error that occurred producing the error page.
+       Link                     string   // A configurable link to wrap the error source in
+}
+
+// SourceLine structure to hold the per-source-line details.
+type SourceLine struct {
+       Source  string
+       Line    int
+       IsError bool
+}
+
+// NewErrorFromPanic method finds the deepest stack from in user code and
+// provide a code listing of that, on the line that eventually triggered
+// the panic.  Returns nil if no relevant stack frame can be found.
+func NewErrorFromPanic(err interface{}) *Error {
+
+       // Parse the filename and line from the originating line of app code.
+       // /Users/robfig/code/gocode/src/revel/examples/booking/app/controllers/hotels.go:191 (0x44735)
+       stack := string(debug.Stack())
+       frame, basePath := findRelevantStackFrame(stack)
+       if frame == -1 {
+               return nil
+       }
+
+       stack = stack[frame:]
+       stackElement := stack[:strings.Index(stack, "\n")]
+       colonIndex := strings.LastIndex(stackElement, ":")
+       filename := stackElement[:colonIndex]
+       var line int
+       fmt.Sscan(stackElement[colonIndex+1:], &line)
+
+       // Show an error page.
+       description := "Unspecified error"
+       if err != nil {
+               description = fmt.Sprint(err)
+       }
+       lines, readErr := ReadLines(filename)
+       if readErr != nil {
+               utilLog.Error("Unable to read file", "file", filename, "error", readErr)
+       }
+       return &Error{
+               Title:       "Runtime Panic",
+               Path:        filename[len(basePath):],
+               Line:        line,
+               Description: description,
+               SourceLines: lines,
+               Stack:       stack,
+       }
+}
+
+// Error method constructs a plaintext version of the error, taking
+// account that fields are optionally set. Returns e.g. Compilation Error
+// (in views/header.html:51): expected right delim in end; got "}"
+func (e *Error) Error() string {
+       loc := ""
+       if e.Path != "" {
+               line := ""
+               if e.Line != 0 {
+                       line = fmt.Sprintf(":%d", e.Line)
+               }
+               loc = fmt.Sprintf("(in %s%s)", e.Path, line)
+       }
+       header := loc
+       if e.Title != "" {
+               if loc != "" {
+                       header = fmt.Sprintf("%s %s: ", e.Title, loc)
+               } else {
+                       header = fmt.Sprintf("%s: ", e.Title)
+               }
+       }
+       return fmt.Sprintf("%s%s Stack: %s", header, e.Description, e.Stack)
+}
+
+// ContextSource method returns a snippet of the source around
+// where the error occurred.
+func (e *Error) ContextSource() []SourceLine {
+       if e.SourceLines == nil {
+               return nil
+       }
+       start := (e.Line - 1) - 5
+       if start < 0 {
+               start = 0
+       }
+       end := (e.Line - 1) + 5
+       if end > len(e.SourceLines) {
+               end = len(e.SourceLines)
+       }
+
+       lines := make([]SourceLine, end-start)
+       for i, src := range e.SourceLines[start:end] {
+               fileLine := start + i + 1
+               lines[i] = SourceLine{src, fileLine, fileLine == e.Line}
+       }
+       return lines
+}
+
+// SetLink method prepares a link and assign to Error.Link attribute
+func (e *Error) SetLink(errorLink string) {
+       errorLink = strings.Replace(errorLink, "{{Path}}", e.Path, -1)
+       errorLink = strings.Replace(errorLink, "{{Line}}", strconv.Itoa(e.Line), -1)
+
+       e.Link = "<a href=" + errorLink + ">" + e.Path + ":" + strconv.Itoa(e.Line) + "</a>"
+}
+
+// Return the character index of the first relevant stack frame, or -1 if none were found.
+// Additionally it returns the base path of the tree in which the identified code resides.
+func findRelevantStackFrame(stack string) (int, string) {
+       // Find first item in SourcePath that isn't in RevelPath.
+       // If first item is in RevelPath, keep track of position, trim and check again.
+       partialStack := stack
+       sourcePath := filepath.ToSlash(SourcePath)
+       revelPath := filepath.ToSlash(RevelPath)
+       sumFrame := 0
+       for {
+               frame := strings.Index(partialStack, sourcePath)
+               revelFrame := strings.Index(partialStack, revelPath)
+
+               if frame == -1 {
+                       break
+               } else if frame != revelFrame {
+                       return sumFrame + frame, SourcePath
+               } else {
+                       // Need to at least trim off the first character so this frame isn't caught again.
+                       partialStack = partialStack[frame+1:]
+                       sumFrame += frame + 1
+               }
+       }
+       for _, module := range Modules {
+               if frame := strings.Index(stack, filepath.ToSlash(module.Path)); frame != -1 {
+                       return frame, module.Path
+               }
+       }
+       return -1, ""
+}
diff --git a/src/foundation/api/revel/event.go b/src/foundation/api/revel/event.go
new file mode 100644 (file)
index 0000000..9c66fb0
--- /dev/null
@@ -0,0 +1,57 @@
+package revel
+
+type (
+       // The event type
+       Event int
+       // The event response
+       EventResponse int
+       // The handler signature
+       EventHandler func(typeOf Event, value interface{}) (responseOf EventResponse)
+)
+
+const (
+       // Event type when templates are going to be refreshed (receivers are registered template engines added to the template.engine conf option)
+       TEMPLATE_REFRESH_REQUESTED Event = iota
+       // Event type when templates are refreshed (receivers are registered template engines added to the template.engine conf option)
+       TEMPLATE_REFRESH_COMPLETED
+       // Event type before all module loads, events thrown to handlers added to AddInitEventHandler
+
+       // Event type before all module loads, events thrown to handlers added to AddInitEventHandler
+       REVEL_BEFORE_MODULES_LOADED
+       // Event type after all module loads, events thrown to handlers added to AddInitEventHandler
+       REVEL_AFTER_MODULES_LOADED
+
+       // Event type before server engine is initialized, receivers are active server engine and handlers added to AddInitEventHandler
+       ENGINE_BEFORE_INITIALIZED
+       // Event type before server engine is started, receivers are active server engine and handlers added to AddInitEventHandler
+       ENGINE_STARTED
+
+       // Event raised when the engine is told to shutdown
+       ENGINE_SHUTDOWN_REQUEST
+
+       // Event type after server engine is stopped, receivers are active server engine and handlers added to AddInitEventHandler
+       ENGINE_SHUTDOWN
+
+       // Called before routes are refreshed
+       ROUTE_REFRESH_REQUESTED
+       // Called after routes have been refreshed
+       ROUTE_REFRESH_COMPLETED
+
+       // Fired when a panic is caught during the startup process
+       REVEL_FAILURE
+)
+
+// Fires system events from revel
+func RaiseEvent(key Event, value interface{}) (response EventResponse) {
+       utilLog.Info("Raising event", "len", len(initEventList))
+       for _, handler := range initEventList {
+               response |= handler(key, value)
+       }
+       return
+}
+
+// Add event handler to listen for all system events
+func AddInitEventHandler(handler EventHandler) {
+       initEventList = append(initEventList, handler)
+       return
+}
diff --git a/src/foundation/api/revel/event_test.go b/src/foundation/api/revel/event_test.go
new file mode 100644 (file)
index 0000000..0baadc0
--- /dev/null
@@ -0,0 +1,24 @@
+package revel_test
+
+import (
+       "github.com/revel/revel"
+       "github.com/stretchr/testify/assert"
+       "testing"
+)
+
+// Test that the event handler can be attached and it dispatches the event received
+func TestEventHandler(t *testing.T) {
+       counter := 0
+       newListener := func(typeOf revel.Event, value interface{}) (responseOf revel.EventResponse) {
+               if typeOf == revel.REVEL_FAILURE {
+                       counter++
+               }
+               return
+       }
+       // Attach the same handlder twice so we expect to see the response twice as well
+       revel.AddInitEventHandler(newListener)
+       revel.AddInitEventHandler(newListener)
+       revel.RaiseEvent(revel.REVEL_AFTER_MODULES_LOADED, nil)
+       revel.RaiseEvent(revel.REVEL_FAILURE, nil)
+       assert.Equal(t, counter, 2, "Expected event handler to have been called")
+}
diff --git a/src/foundation/api/revel/fakeapp_test.go b/src/foundation/api/revel/fakeapp_test.go
new file mode 100644 (file)
index 0000000..a2bcabd
--- /dev/null
@@ -0,0 +1,152 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "os"
+       "path/filepath"
+       "reflect"
+)
+
+type Hotel struct {
+       HotelID          int
+       Name, Address    string
+       City, State, Zip string
+       Country          string
+       Price            int
+}
+
+type Hotels struct {
+       *Controller
+}
+
+type Static struct {
+       *Controller
+}
+
+type Implicit struct {
+       *Controller
+}
+
+type Application struct {
+       *Controller
+}
+
+func (c Hotels) Show(id int) Result {
+       title := "View Hotel"
+       hotel := &Hotel{id, "A Hotel", "300 Main St.", "New York", "NY", "10010", "USA", 300}
+       // The line number below must match the one with the code : RenderArgNames: map[int][]string{43: {"title", "hotel"}},
+       return c.Render(title, hotel)
+}
+
+func (c Hotels) Book(id int) Result {
+       hotel := &Hotel{id, "A Hotel", "300 Main St.", "New York", "NY", "10010", "USA", 300}
+       return c.RenderJSON(hotel)
+}
+
+func (c Hotels) Index() Result {
+       return c.RenderText("Hello, World!")
+}
+
+func (c Static) Serve(prefix, path string) Result {
+       var basePath, dirName string
+
+       if !filepath.IsAbs(dirName) {
+               basePath = BasePath
+       }
+
+       fname := filepath.Join(basePath, prefix, path)
+       file, err := os.Open(fname)
+       if os.IsNotExist(err) {
+               return c.NotFound("")
+       } else if err != nil {
+               RevelLog.Errorf("Problem opening file (%s): %s ", fname, err)
+               return c.NotFound("This was found but not sure why we couldn't open it.")
+       }
+       return c.RenderFile(file, "")
+}
+
+// Register controllers is in its own function so the route test can use it as well
+func registerControllers() {
+       controllers = make(map[string]*ControllerType)
+       RaiseEvent(ROUTE_REFRESH_REQUESTED, nil)
+       RegisterController((*Hotels)(nil),
+               []*MethodType{
+                       {
+                               Name: "Index",
+                       },
+                       {
+                               Name: "Show",
+                               Args: []*MethodArg{
+                                       {"id", reflect.TypeOf((*int)(nil))},
+                               },
+                               RenderArgNames: map[int][]string{41: {"title", "hotel"}},
+                       },
+                       {
+                               Name: "Book",
+                               Args: []*MethodArg{
+                                       {"id", reflect.TypeOf((*int)(nil))},
+                               },
+                       },
+               })
+
+       RegisterController((*Static)(nil),
+               []*MethodType{
+                       {
+                               Name: "Serve",
+                               Args: []*MethodArg{
+                                       {Name: "prefix", Type: reflect.TypeOf((*string)(nil))},
+                                       {Name: "filepath", Type: reflect.TypeOf((*string)(nil))},
+                               },
+                               RenderArgNames: map[int][]string{},
+                       },
+               })
+       RegisterController((*Implicit)(nil),
+               []*MethodType{
+                       {
+                               Name: "Implicit",
+                               Args: []*MethodArg{
+                                       {Name: "prefix", Type: reflect.TypeOf((*string)(nil))},
+                                       {Name: "filepath", Type: reflect.TypeOf((*string)(nil))},
+                               },
+                               RenderArgNames: map[int][]string{},
+                       },
+               })
+       RegisterController((*Application)(nil),
+               []*MethodType{
+                       {
+                               Name: "Application",
+                               Args: []*MethodArg{
+                                       {Name: "prefix", Type: reflect.TypeOf((*string)(nil))},
+                                       {Name: "filepath", Type: reflect.TypeOf((*string)(nil))},
+                               },
+                               RenderArgNames: map[int][]string{},
+                       },
+                       {
+                               Name: "Index",
+                               Args: []*MethodArg{
+                                       {Name: "foo", Type: reflect.TypeOf((*string)(nil))},
+                                       {Name: "bar", Type: reflect.TypeOf((*string)(nil))},
+                               },
+                               RenderArgNames: map[int][]string{},
+                       },
+               })
+}
+func startFakeBookingApp() {
+       Init("prod", "github.com/revel/revel/testdata", "")
+
+       MainTemplateLoader = NewTemplateLoader([]string{ViewsPath, filepath.Join(RevelPath, "templates")})
+       if err := MainTemplateLoader.Refresh(); err != nil {
+               RevelLog.Fatal("Template error","error",err)
+       }
+
+       registerControllers()
+
+       InitServerEngine(9000, GO_NATIVE_SERVER_ENGINE)
+       RaiseEvent(ENGINE_BEFORE_INITIALIZED, nil)
+       InitServer()
+
+       RaiseEvent(ENGINE_STARTED, nil)
+}
diff --git a/src/foundation/api/revel/field.go b/src/foundation/api/revel/field.go
new file mode 100644 (file)
index 0000000..93f2b1c
--- /dev/null
@@ -0,0 +1,108 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "reflect"
+       "strings"
+)
+
+// Field represents a data field that may be collected in a web form.
+type Field struct {
+       Name       string
+       Error      *ValidationError
+       viewArgs   map[string]interface{}
+       controller *Controller
+}
+
+func NewField(name string, viewArgs map[string]interface{}) *Field {
+       err, _ := viewArgs["errors"].(map[string]*ValidationError)[name]
+       controller, _ := viewArgs["_controller"].(*Controller)
+       return &Field{
+               Name:       name,
+               Error:      err,
+               viewArgs:   viewArgs,
+               controller: controller,
+       }
+}
+
+// ID returns an identifier suitable for use as an HTML id.
+func (f *Field) ID() string {
+       return strings.Replace(f.Name, ".", "_", -1)
+}
+
+// Flash returns the flashed value of this Field.
+func (f *Field) Flash() string {
+       v, _ := f.viewArgs["flash"].(map[string]string)[f.Name]
+       return v
+}
+
+// Options returns the option list of this Field.
+func (f *Field) Options() []string {
+       if f.viewArgs["options"] == nil {
+               return nil
+       }
+       v, _ := f.viewArgs["options"].(map[string][]string)[f.Name]
+       return v
+}
+
+// FlashArray returns the flashed value of this Field as a list split on comma.
+func (f *Field) FlashArray() []string {
+       v := f.Flash()
+       if v == "" {
+               return []string{}
+       }
+       return strings.Split(v, ",")
+}
+
+// Value returns the current value of this Field.
+func (f *Field) Value() interface{} {
+       pieces := strings.Split(f.Name, ".")
+       answer, ok := f.viewArgs[pieces[0]]
+       if !ok {
+               return ""
+       }
+
+       val := reflect.ValueOf(answer)
+       for i := 1; i < len(pieces); i++ {
+               if val.Kind() == reflect.Ptr {
+                       val = val.Elem()
+               }
+               val = val.FieldByName(pieces[i])
+               if !val.IsValid() {
+                       return ""
+               }
+       }
+
+       return val.Interface()
+}
+
+// ErrorClass returns ErrorCSSClass if this field has a validation error, else empty string.
+func (f *Field) ErrorClass() string {
+       if f.Error != nil {
+               if errorClass, ok := f.viewArgs["ERROR_CLASS"]; ok {
+                       return errorClass.(string)
+               }
+               return ErrorCSSClass
+       }
+       return ""
+}
+
+// Get the short name and translate it
+func (f *Field) ShortName() string {
+       name := f.Name
+       if i := strings.LastIndex(name, "."); i > 0 {
+               name = name[i+1:]
+       }
+       return f.Translate(name)
+}
+
+// Translate the text
+func (f *Field) Translate(text string, args ...interface{}) string {
+       if f.controller != nil {
+               text = f.controller.Message(text, args...)
+       }
+       return text
+}
diff --git a/src/foundation/api/revel/filter.go b/src/foundation/api/revel/filter.go
new file mode 100644 (file)
index 0000000..2226dff
--- /dev/null
@@ -0,0 +1,31 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+// Filter type definition for Revel's filter
+type Filter func(c *Controller, filterChain []Filter)
+
+// Filters is the default set of global filters.
+// It may be set by the application on initialization.
+var Filters = []Filter{
+       PanicFilter,             // Recover from panics and display an error page instead.
+       RouterFilter,            // Use the routing table to select the right Action.
+       FilterConfiguringFilter, // A hook for adding or removing per-Action filters.
+       ParamsFilter,            // Parse parameters into Controller.Params.
+       SessionFilter,           // Restore and write the session cookie.
+       FlashFilter,             // Restore and write the flash cookie.
+       ValidationFilter,        // Restore kept validation errors and save new ones from cookie.
+       I18nFilter,              // Resolve the requested language.
+       InterceptorFilter,       // Run interceptors around the action.
+       CompressFilter,          // Compress the result.
+       BeforeAfterFilter,
+       ActionInvoker, // Invoke the action.
+}
+
+// NilFilter and NilChain are helpful in writing filter tests.
+var (
+       NilFilter = func(_ *Controller, _ []Filter) {}
+       NilChain  = []Filter{NilFilter}
+)
diff --git a/src/foundation/api/revel/filterconfig.go b/src/foundation/api/revel/filterconfig.go
new file mode 100644 (file)
index 0000000..010d7ea
--- /dev/null
@@ -0,0 +1,223 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "reflect"
+       "strings"
+)
+
+// Map from "Controller" or "Controller.Method" to the Filter chain
+var filterOverrides = make(map[string][]Filter)
+
+// FilterConfigurator allows the developer configure the filter chain on a
+// per-controller or per-action basis.  The filter configuration is applied by
+// the FilterConfiguringFilter, which is itself a filter stage.  For example,
+//
+// Assuming:
+//   Filters = []Filter{
+//     RouterFilter,
+//     FilterConfiguringFilter,
+//     SessionFilter,
+//     ActionInvoker,
+//   }
+//
+// Add:
+//   FilterAction(App.Action).
+//     Add(OtherFilter)
+//
+//   => RouterFilter, FilterConfiguringFilter, SessionFilter, OtherFilter, ActionInvoker
+//
+// Remove:
+//   FilterAction(App.Action).
+//     Remove(SessionFilter)
+//
+//   => RouterFilter, FilterConfiguringFilter, OtherFilter, ActionInvoker
+//
+// Insert:
+//   FilterAction(App.Action).
+//     Insert(OtherFilter, revel.BEFORE, SessionFilter)
+//
+//   => RouterFilter, FilterConfiguringFilter, OtherFilter, SessionFilter, ActionInvoker
+//
+// Filter modifications may be combined between Controller and Action.  For example:
+//   FilterController(App{}).
+//     Add(Filter1)
+//   FilterAction(App.Action).
+//     Add(Filter2)
+//
+//  .. would result in App.Action being filtered by both Filter1 and Filter2.
+//
+// Note: the last filter stage is not subject to the configurator.  In
+// particular, Add() adds a filter to the second-to-last place.
+type FilterConfigurator struct {
+       key            string // e.g. "App", "App.Action"
+       controllerName string // e.g. "App"
+}
+
+func newFilterConfigurator(controllerName, methodName string) FilterConfigurator {
+       if methodName == "" {
+               return FilterConfigurator{controllerName, controllerName}
+       }
+       return FilterConfigurator{controllerName + "." + methodName, controllerName}
+}
+
+// FilterController returns a configurator for the filters applied to all
+// actions on the given controller instance.  For example:
+//   FilterController(MyController{})
+func FilterController(controllerInstance interface{}) FilterConfigurator {
+       t := reflect.TypeOf(controllerInstance)
+       for t.Kind() == reflect.Ptr {
+               t = t.Elem()
+       }
+       return newFilterConfigurator(t.Name(), "")
+}
+
+// FilterAction returns a configurator for the filters applied to the given
+// controller method. For example:
+//   FilterAction(MyController.MyAction)
+func FilterAction(methodRef interface{}) FilterConfigurator {
+       var (
+               methodValue = reflect.ValueOf(methodRef)
+               methodType  = methodValue.Type()
+       )
+       if methodType.Kind() != reflect.Func || methodType.NumIn() == 0 {
+               panic("Expecting a controller method reference (e.g. Controller.Action), got a " +
+                       methodType.String())
+       }
+
+       controllerType := methodType.In(0)
+       method := FindMethod(controllerType, methodValue)
+       if method == nil {
+               panic("Action not found on controller " + controllerType.Name())
+       }
+
+       for controllerType.Kind() == reflect.Ptr {
+               controllerType = controllerType.Elem()
+       }
+
+       return newFilterConfigurator(controllerType.Name(), method.Name)
+}
+
+// Add the given filter in the second-to-last position in the filter chain.
+// (Second-to-last so that it is before ActionInvoker)
+func (conf FilterConfigurator) Add(f Filter) FilterConfigurator {
+       conf.apply(func(fc []Filter) []Filter {
+               return conf.addFilter(f, fc)
+       })
+       return conf
+}
+
+func (conf FilterConfigurator) addFilter(f Filter, fc []Filter) []Filter {
+       return append(fc[:len(fc)-1], f, fc[len(fc)-1])
+}
+
+// Remove a filter from the filter chain.
+func (conf FilterConfigurator) Remove(target Filter) FilterConfigurator {
+       conf.apply(func(fc []Filter) []Filter {
+               return conf.rmFilter(target, fc)
+       })
+       return conf
+}
+
+func (conf FilterConfigurator) rmFilter(target Filter, fc []Filter) []Filter {
+       for i, f := range fc {
+               if FilterEq(f, target) {
+                       return append(fc[:i], fc[i+1:]...)
+               }
+       }
+       return fc
+}
+
+// Insert a filter into the filter chain before or after another.
+// This may be called with the BEFORE or AFTER constants, for example:
+//   revel.FilterAction(App.Index).
+//     Insert(MyFilter, revel.BEFORE, revel.ActionInvoker).
+//     Insert(MyFilter2, revel.AFTER, revel.PanicFilter)
+func (conf FilterConfigurator) Insert(insert Filter, where When, target Filter) FilterConfigurator {
+       if where != BEFORE && where != AFTER {
+               panic("where must be BEFORE or AFTER")
+       }
+       conf.apply(func(fc []Filter) []Filter {
+               return conf.insertFilter(insert, where, target, fc)
+       })
+       return conf
+}
+
+func (conf FilterConfigurator) insertFilter(insert Filter, where When, target Filter, fc []Filter) []Filter {
+       for i, f := range fc {
+               if FilterEq(f, target) {
+                       if where == BEFORE {
+                               return append(fc[:i], append([]Filter{insert}, fc[i:]...)...)
+                       }
+                       return append(fc[:i+1], append([]Filter{insert}, fc[i+1:]...)...)
+               }
+       }
+       return fc
+}
+
+// getChain returns the filter chain that applies to the given controller or
+// action.  If no overrides are configured, then a copy of the default filter
+// chain is returned.
+func (conf FilterConfigurator) getChain() []Filter {
+       var filters []Filter
+       if filters = getOverrideChain(conf.controllerName, conf.key); filters == nil {
+               // The override starts with all filters after FilterConfiguringFilter
+               for i, f := range Filters {
+                       if FilterEq(f, FilterConfiguringFilter) {
+                               filters = make([]Filter, len(Filters)-i-1)
+                               copy(filters, Filters[i+1:])
+                               break
+                       }
+               }
+               if filters == nil {
+                       panic("FilterConfiguringFilter not found in revel.Filters.")
+               }
+       }
+       return filters
+}
+
+// apply applies the given functional change to the filter overrides.
+// No other function modifies the filterOverrides map.
+func (conf FilterConfigurator) apply(f func([]Filter) []Filter) {
+       // Updates any actions that have had their filters overridden, if this is a
+       // Controller configurator.
+       if conf.controllerName == conf.key {
+               for k, v := range filterOverrides {
+                       if strings.HasPrefix(k, conf.controllerName+".") {
+                               filterOverrides[k] = f(v)
+                       }
+               }
+       }
+
+       // Update the Controller or Action overrides.
+       filterOverrides[conf.key] = f(conf.getChain())
+}
+
+// FilterEq returns true if the two filters reference the same filter.
+func FilterEq(a, b Filter) bool {
+       return reflect.ValueOf(a).Pointer() == reflect.ValueOf(b).Pointer()
+}
+
+// FilterConfiguringFilter is a filter stage that customizes the remaining
+// filter chain for the action being invoked.
+func FilterConfiguringFilter(c *Controller, fc []Filter) {
+       if newChain := getOverrideChain(c.Name, c.Action); newChain != nil {
+               newChain[0](c, newChain[1:])
+               return
+       }
+       fc[0](c, fc[1:])
+}
+
+// getOverrideChain retrieves the overrides for the action that is set
+func getOverrideChain(controllerName, action string) []Filter {
+       if newChain, ok := filterOverrides[action]; ok {
+               return newChain
+       }
+       if newChain, ok := filterOverrides[controllerName]; ok {
+               return newChain
+       }
+       return nil
+}
diff --git a/src/foundation/api/revel/filterconfig_test.go b/src/foundation/api/revel/filterconfig_test.go
new file mode 100644 (file)
index 0000000..a3973f9
--- /dev/null
@@ -0,0 +1,141 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import "testing"
+
+type FakeController struct{}
+
+func (c FakeController) Foo()  {}
+func (c *FakeController) Bar() {}
+
+func TestFilterConfiguratorKey(t *testing.T) {
+       conf := FilterController(FakeController{})
+       if conf.key != "FakeController" {
+               t.Errorf("Expected key 'FakeController', was %s", conf.key)
+       }
+
+       conf = FilterController(&FakeController{})
+       if conf.key != "FakeController" {
+               t.Errorf("Expected key 'FakeController', was %s", conf.key)
+       }
+
+       conf = FilterAction(FakeController.Foo)
+       if conf.key != "FakeController.Foo" {
+               t.Errorf("Expected key 'FakeController.Foo', was %s", conf.key)
+       }
+
+       conf = FilterAction((*FakeController).Bar)
+       if conf.key != "FakeController.Bar" {
+               t.Errorf("Expected key 'FakeController.Bar', was %s", conf.key)
+       }
+}
+
+func TestFilterConfigurator(t *testing.T) {
+       // Filters is global state.  Restore it after this test.
+       oldFilters := make([]Filter, len(Filters))
+       copy(oldFilters, Filters)
+       defer func() {
+               Filters = oldFilters
+       }()
+
+       Filters = []Filter{
+               RouterFilter,
+               FilterConfiguringFilter,
+               SessionFilter,
+               FlashFilter,
+               ActionInvoker,
+       }
+
+       // Do one of each operation.
+       conf := FilterAction(FakeController.Foo).
+               Add(NilFilter).
+               Remove(FlashFilter).
+               Insert(ValidationFilter, BEFORE, NilFilter).
+               Insert(I18nFilter, AFTER, NilFilter)
+       expected := []Filter{
+               SessionFilter,
+               ValidationFilter,
+               NilFilter,
+               I18nFilter,
+               ActionInvoker,
+       }
+       actual := getOverride("Foo")
+       if len(actual) != len(expected) || !filterSliceEqual(actual, expected) {
+               t.Errorf("Ops failed.\nActual: %#v\nExpect: %#v\nConf:%v", actual, expected, conf)
+       }
+
+       // Action2 should be unchanged
+       if getOverride("Bar") != nil {
+               t.Errorf("Filtering Action should not affect Action2.")
+       }
+
+       // Test that combining overrides on both the Controller and Action works.
+       FilterController(FakeController{}).
+               Add(PanicFilter)
+       expected = []Filter{
+               SessionFilter,
+               ValidationFilter,
+               NilFilter,
+               I18nFilter,
+               PanicFilter,
+               ActionInvoker,
+       }
+       actual = getOverride("Foo")
+       if len(actual) != len(expected) || !filterSliceEqual(actual, expected) {
+               t.Errorf("Expected PanicFilter added to Foo.\nActual: %#v\nExpect: %#v", actual, expected)
+       }
+
+       expected = []Filter{
+               SessionFilter,
+               FlashFilter,
+               PanicFilter,
+               ActionInvoker,
+       }
+       actual = getOverride("Bar")
+       if len(actual) != len(expected) || !filterSliceEqual(actual, expected) {
+               t.Errorf("Expected PanicFilter added to Bar.\nActual: %#v\nExpect: %#v", actual, expected)
+       }
+
+       FilterAction((*FakeController).Bar).
+               Add(NilFilter)
+       expected = []Filter{
+               SessionFilter,
+               ValidationFilter,
+               NilFilter,
+               I18nFilter,
+               PanicFilter,
+               ActionInvoker,
+       }
+       actual = getOverride("Foo")
+       if len(actual) != len(expected) || !filterSliceEqual(actual, expected) {
+               t.Errorf("Expected no change to Foo.\nActual: %#v\nExpect: %#v", actual, expected)
+       }
+
+       expected = []Filter{
+               SessionFilter,
+               FlashFilter,
+               PanicFilter,
+               NilFilter,
+               ActionInvoker,
+       }
+       actual = getOverride("Bar")
+       if len(actual) != len(expected) || !filterSliceEqual(actual, expected) {
+               t.Errorf("Expected NilFilter added to Bar.\nActual: %#v\nExpect: %#v", actual, expected)
+       }
+}
+
+func filterSliceEqual(a, e []Filter) bool {
+       for i, f := range a {
+               if !FilterEq(f, e[i]) {
+                       return false
+               }
+       }
+       return true
+}
+
+func getOverride(methodName string) []Filter {
+       return getOverrideChain("FakeController", "FakeController."+methodName)
+}
diff --git a/src/foundation/api/revel/flash.go b/src/foundation/api/revel/flash.go
new file mode 100644 (file)
index 0000000..b27f5e0
--- /dev/null
@@ -0,0 +1,78 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "fmt"
+       "net/http"
+       "net/url"
+)
+
+// Flash represents a cookie that is overwritten on each request.
+// It allows data to be stored across one page at a time.
+// This is commonly used to implement success or error messages.
+// E.g. the Post/Redirect/Get pattern:
+// http://en.wikipedia.org/wiki/Post/Redirect/Get
+type Flash struct {
+       // `Data` is the input which is read in `restoreFlash`, `Out` is the output which is set in a FLASH cookie at the end of the `FlashFilter()`
+       Data, Out map[string]string
+}
+
+// Error serializes the given msg and args to an "error" key within
+// the Flash cookie.
+func (f Flash) Error(msg string, args ...interface{}) {
+       if len(args) == 0 {
+               f.Out["error"] = msg
+       } else {
+               f.Out["error"] = fmt.Sprintf(msg, args...)
+       }
+}
+
+// Success serializes the given msg and args to a "success" key within
+// the Flash cookie.
+func (f Flash) Success(msg string, args ...interface{}) {
+       if len(args) == 0 {
+               f.Out["success"] = msg
+       } else {
+               f.Out["success"] = fmt.Sprintf(msg, args...)
+       }
+}
+
+// FlashFilter is a Revel Filter that retrieves and sets the flash cookie.
+// Within Revel, it is available as a Flash attribute on Controller instances.
+// The name of the Flash cookie is set as CookiePrefix + "_FLASH".
+func FlashFilter(c *Controller, fc []Filter) {
+       c.Flash = restoreFlash(c.Request)
+       c.ViewArgs["flash"] = c.Flash.Data
+
+       fc[0](c, fc[1:])
+
+       // Store the flash.
+       var flashValue string
+       for key, value := range c.Flash.Out {
+               flashValue += "\x00" + key + ":" + value + "\x00"
+       }
+       c.SetCookie(&http.Cookie{
+               Name:     CookiePrefix + "_FLASH",
+               Value:    url.QueryEscape(flashValue),
+               HttpOnly: true,
+               Secure:   CookieSecure,
+               Path:     "/",
+       })
+}
+
+// restoreFlash deserializes a Flash cookie struct from a request.
+func restoreFlash(req *Request) Flash {
+       flash := Flash{
+               Data: make(map[string]string),
+               Out:  make(map[string]string),
+       }
+       if cookie, err := req.Cookie(CookiePrefix + "_FLASH"); err == nil {
+               ParseKeyValueCookie(cookie.GetValue(), func(key, val string) {
+                       flash.Data[key] = val
+               })
+       }
+       return flash
+}
diff --git a/src/foundation/api/revel/http.go b/src/foundation/api/revel/http.go
new file mode 100644 (file)
index 0000000..dbdd58d
--- /dev/null
@@ -0,0 +1,489 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "bytes"
+       "errors"
+       "fmt"
+       "io"
+       "net/http"
+       "net/url"
+       "sort"
+       "strconv"
+       "strings"
+
+       "context"
+       "mime/multipart"
+       "path/filepath"
+)
+
+// Request is Revel's HTTP request object structure
+type Request struct {
+       In              ServerRequest   // The server request
+       Header          *RevelHeader    // The revel header
+       ContentType     string          // The content type
+       Format          string          // The output format "html", "xml", "json", or "txt"
+       AcceptLanguages AcceptLanguages // The languages to accept
+       Locale          string          // THe locale
+       WebSocket       ServerWebSocket // The websocket
+       Method          string          // The method
+       RemoteAddr      string          // The remote address
+       Host            string          // The host
+       // URL request path from the server (built)
+       URL *url.URL // The url
+       // DEPRECATED use GetForm()
+       Form url.Values // The Form
+       // DEPRECATED use GetMultipartForm()
+       MultipartForm *MultipartForm // The multipart form
+       controller    *Controller    // The controller, so some of this data can be fetched
+}
+
+var FORM_NOT_FOUND = errors.New("Form Not Found")
+var httpLog = RevelLog.New("section", "http")
+
+// Response is Revel's HTTP response object structure
+type Response struct {
+       Status      int
+       ContentType string
+       Out         OutResponse
+       writer      io.Writer
+}
+
+// The output response
+type OutResponse struct {
+       // internalHeader.Server Set by ServerResponse.Get(HTTP_SERVER_HEADER), saves calling the get every time the header needs to be written to
+       internalHeader *RevelHeader   // The internal header
+       Server         ServerResponse // The server response
+       response       *Response      // The response
+}
+
+// The header defined in Revel
+type RevelHeader struct {
+       Server ServerHeader // The server
+}
+
+// NewResponse wraps ServerResponse inside a Revel's Response and returns it
+func NewResponse(w ServerResponse) (r *Response) {
+       r = &Response{Out: OutResponse{Server: w, internalHeader: &RevelHeader{}}}
+       r.Out.response = r
+       return r
+}
+
+// NewRequest returns a Revel's HTTP request instance with given HTTP instance
+func NewRequest(r ServerRequest) *Request {
+       req := &Request{Header: &RevelHeader{}}
+       if r != nil {
+               req.SetRequest(r)
+       }
+       return req
+}
+func (req *Request) SetRequest(r ServerRequest) {
+       req.In = r
+       if h, e := req.In.Get(HTTP_SERVER_HEADER); e == nil {
+               req.Header.Server = h.(ServerHeader)
+       }
+
+       req.URL, _ = req.GetValue(HTTP_URL).(*url.URL)
+       req.ContentType = ResolveContentType(req)
+       req.Format = ResolveFormat(req)
+       req.AcceptLanguages = ResolveAcceptLanguage(req)
+       req.Method, _ = req.GetValue(HTTP_METHOD).(string)
+       req.RemoteAddr, _ = req.GetValue(HTTP_REMOTE_ADDR).(string)
+       req.Host, _ = req.GetValue(HTTP_HOST).(string)
+
+}
+
+// Returns a cookie
+func (req *Request) Cookie(key string) (ServerCookie, error) {
+       if req.Header.Server != nil {
+               return req.Header.Server.GetCookie(key)
+       }
+       return nil, http.ErrNoCookie
+}
+
+// Fetch the requested URI
+func (req *Request) GetRequestURI() string {
+       uri, _ := req.GetValue(HTTP_REQUEST_URI).(string)
+       return uri
+}
+
+// Fetch the query
+func (req *Request) GetQuery() (v url.Values) {
+       v, _ = req.GetValue(ENGINE_PARAMETERS).(url.Values)
+       return
+}
+
+// Fetch the path
+func (req *Request) GetPath() (path string) {
+       path, _ = req.GetValue(ENGINE_PATH).(string)
+       return
+}
+
+// Fetch the body
+func (req *Request) GetBody() (body io.Reader) {
+       body, _ = req.GetValue(HTTP_BODY).(io.Reader)
+       return
+}
+
+// Fetch the context
+func (req *Request) Context() (c context.Context) {
+       c, _ = req.GetValue(HTTP_REQUEST_CONTEXT).(context.Context)
+       return
+}
+
+// Deprecated use controller.Params.Get()
+func (req *Request) FormValue(key string) (value string) {
+       return req.controller.Params.Get(key)
+}
+
+// Deprecated use controller.Params.Form[Key]
+func (req *Request) PostFormValue(key string) (value string) {
+       valueList := req.controller.Params.Form[key]
+       if len(valueList) > 0 {
+               value = valueList[0]
+       }
+       return
+}
+
+// Deprecated use GetForm() instead
+func (req *Request) ParseForm() (e error) {
+       if req.Form == nil {
+               req.Form, e = req.GetForm()
+       }
+       return
+}
+
+func (req *Request) GetForm() (url.Values, error) {
+       if form, err := req.In.Get(HTTP_FORM); err != nil {
+               return nil, err
+       } else if values, found := form.(url.Values); found {
+               req.Form = values
+               return values, nil
+       }
+       return nil, FORM_NOT_FOUND
+}
+
+// Deprecated for backwards compatibility only
+type MultipartForm struct {
+       File   map[string][]*multipart.FileHeader
+       Value  url.Values
+       origin ServerMultipartForm
+}
+
+func (req *Request) MultipartReader() (*multipart.Reader, error) {
+
+       return nil, errors.New("MultipartReader not supported, use controller.Param")
+}
+
+// Deprecated for backwards compatibility only
+func newMultipareForm(s ServerMultipartForm) (f *MultipartForm) {
+       return &MultipartForm{File: s.GetFiles(), Value: s.GetValues(), origin: s}
+}
+
+// Deprecated use GetMultipartForm() instead
+func (req *Request) ParseMultipartForm(_ int64) (e error) {
+       var s ServerMultipartForm
+       if s, e = req.GetMultipartForm(); e == nil {
+               req.MultipartForm = newMultipareForm(s)
+       }
+       return
+}
+
+// Return the args for the controller
+func (req *Request) Args() map[string]interface{} {
+       return req.controller.Args
+}
+
+// Return a multipart form
+func (req *Request) GetMultipartForm() (ServerMultipartForm, error) {
+       if form, err := req.In.Get(HTTP_MULTIPART_FORM); err != nil {
+               return nil, err
+       } else if values, found := form.(ServerMultipartForm); found {
+               return values, nil
+       }
+       return nil, FORM_NOT_FOUND
+}
+
+// Destroy the request
+func (req *Request) Destroy() {
+       req.In = nil
+       req.ContentType = ""
+       req.Format = ""
+       req.AcceptLanguages = nil
+       req.Method = ""
+       req.RemoteAddr = ""
+       req.Host = ""
+       req.Header.Destroy()
+       req.URL = nil
+       req.Form = nil
+       req.MultipartForm = nil
+}
+
+// Set the server response
+func (resp *Response) SetResponse(r ServerResponse) {
+       resp.Out.Server = r
+       if h, e := r.Get(HTTP_SERVER_HEADER); e == nil {
+               resp.Out.internalHeader.Server, _ = h.(ServerHeader)
+       }
+}
+
+// Destroy the output response
+func (o *OutResponse) Destroy() {
+       o.response = nil
+       o.internalHeader.Destroy()
+}
+
+// Destroy the RevelHeader
+func (h *RevelHeader) Destroy() {
+       h.Server = nil
+}
+
+// Destroy the Response
+func (resp *Response) Destroy() {
+       resp.Out.Destroy()
+       resp.Status = 0
+       resp.ContentType = ""
+       resp.writer = nil
+}
+
+// UserAgent returns the client's User-Agent header string.
+func (r *Request) UserAgent() string {
+       return r.Header.Get("User-Agent")
+}
+
+// Referer returns the client's Referer header string.
+func (req *Request) Referer() string {
+       return req.Header.Get("Referer")
+}
+
+// Return the httpheader for the key
+func (req *Request) GetHttpHeader(key string) string {
+       return req.Header.Get(key)
+}
+
+// Return the value from the server
+func (r *Request) GetValue(key int) (value interface{}) {
+       value, _ = r.In.Get(key)
+       return
+}
+
+// WriteHeader writes the header (for now, just the status code).
+// The status may be set directly by the application (c.Response.Status = 501).
+// If it isn't, then fall back to the provided status code.
+func (resp *Response) WriteHeader(defaultStatusCode int, defaultContentType string) {
+       if resp.ContentType == "" {
+               resp.ContentType = defaultContentType
+       }
+       resp.Out.internalHeader.Set("Content-Type", resp.ContentType)
+       if resp.Status == 0 {
+               resp.Status = defaultStatusCode
+       }
+       resp.SetStatus(resp.Status)
+}
+func (resp *Response) SetStatus(statusCode int) {
+       if resp.Out.internalHeader.Server != nil {
+               resp.Out.internalHeader.Server.SetStatus(statusCode)
+       } else {
+               resp.Out.Server.Set(ENGINE_RESPONSE_STATUS, statusCode)
+       }
+
+}
+
+// Return the writer
+func (resp *Response) GetWriter() (writer io.Writer) {
+       writer = resp.writer
+       if writer == nil {
+               if w, e := resp.Out.Server.Get(ENGINE_WRITER); e == nil {
+                       writer, resp.writer = w.(io.Writer), w.(io.Writer)
+               }
+       }
+
+       return
+}
+
+// Replace the writer
+func (resp *Response) SetWriter(writer io.Writer) bool {
+       resp.writer = writer
+       // Leave it up to the engine to flush and close the writer
+       return resp.Out.Server.Set(ENGINE_WRITER, writer)
+}
+
+// Passes full control to the response to the caller - terminates any initial writes
+func (resp *Response) GetStreamWriter() (writer StreamWriter) {
+       if w, e := resp.Out.Server.Get(HTTP_STREAM_WRITER); e == nil {
+               writer = w.(StreamWriter)
+       }
+       return
+}
+
+// Return the header
+func (o *OutResponse) Header() *RevelHeader {
+       return o.internalHeader
+}
+
+// Write the header out
+func (o *OutResponse) Write(data []byte) (int, error) {
+       return o.response.GetWriter().Write(data)
+}
+
+// Set a value in the header
+func (h *RevelHeader) Set(key, value string) {
+       if h.Server != nil {
+               h.Server.Set(key, value)
+       }
+}
+
+// Add a key to the header
+func (h *RevelHeader) Add(key, value string) {
+       if h.Server != nil {
+               h.Server.Add(key, value)
+       }
+}
+
+// Set a cookie in the header
+func (h *RevelHeader) SetCookie(cookie string) {
+       if h.Server != nil {
+               h.Server.SetCookie(cookie)
+       }
+}
+
+// Set the status for the header
+func (h *RevelHeader) SetStatus(status int) {
+       if h.Server != nil {
+               h.Server.SetStatus(status)
+       }
+}
+
+// Get a key from the header
+func (h *RevelHeader) Get(key string) (value string) {
+       values := h.GetAll(key)
+       if len(values) > 0 {
+               value = values[0]
+       }
+       return
+}
+
+// GetAll returns []string of items (the header split by a comma)
+func (h *RevelHeader) GetAll(key string) (values []string) {
+       if h.Server != nil {
+               values = h.Server.Get(key)
+       }
+       return
+}
+
+// ResolveContentType gets the content type.
+// e.g. From "multipart/form-data; boundary=--" to "multipart/form-data"
+// If none is specified, returns "text/html" by default.
+func ResolveContentType(req *Request) string {
+
+       contentType := req.Header.Get("Content-Type")
+       if contentType == "" {
+               return "text/html"
+       }
+       return strings.ToLower(strings.TrimSpace(strings.Split(contentType, ";")[0]))
+}
+
+// ResolveFormat maps the request's Accept MIME type declaration to
+// a Request.Format attribute, specifically "html", "xml", "json", or "txt",
+// returning a default of "html" when Accept header cannot be mapped to a
+// value above.
+func ResolveFormat(req *Request) string {
+       ext := strings.ToLower(filepath.Ext(req.GetPath()))
+       switch ext {
+       case ".html":
+               return "html"
+       case ".json":
+               return "json"
+       case ".xml":
+               return "xml"
+       case ".txt":
+               return "txt"
+       }
+
+       accept := req.GetHttpHeader("accept")
+
+       switch {
+       case accept == "",
+               strings.HasPrefix(accept, "*/*"), // */
+               strings.Contains(accept, "application/xhtml"),
+               strings.Contains(accept, "text/html"):
+               return "html"
+       case strings.Contains(accept, "application/json"),
+               strings.Contains(accept, "text/javascript"),
+               strings.Contains(accept, "application/javascript"):
+               return "json"
+       case strings.Contains(accept, "application/xml"),
+               strings.Contains(accept, "text/xml"):
+               return "xml"
+       case strings.Contains(accept, "text/plain"):
+               return "txt"
+       }
+
+       return "html"
+}
+
+// AcceptLanguage is a single language from the Accept-Language HTTP header.
+type AcceptLanguage struct {
+       Language string
+       Quality  float32
+}
+
+// AcceptLanguages is collection of sortable AcceptLanguage instances.
+type AcceptLanguages []AcceptLanguage
+
+func (al AcceptLanguages) Len() int           { return len(al) }
+func (al AcceptLanguages) Swap(i, j int)      { al[i], al[j] = al[j], al[i] }
+func (al AcceptLanguages) Less(i, j int) bool { return al[i].Quality > al[j].Quality }
+func (al AcceptLanguages) String() string {
+       output := bytes.NewBufferString("")
+       for i, language := range al {
+               if _, err := output.WriteString(fmt.Sprintf("%s (%1.1f)", language.Language, language.Quality)); err != nil {
+                       httpLog.Error("String: WriteString failed:", "error", err)
+               }
+               if i != len(al)-1 {
+                       if _, err := output.WriteString(", "); err != nil {
+                               httpLog.Error("String: WriteString failed:", "error", err)
+                       }
+               }
+       }
+       return output.String()
+}
+
+// ResolveAcceptLanguage returns a sorted list of Accept-Language
+// header values.
+//
+// The results are sorted using the quality defined in the header for each
+// language range with the most qualified language range as the first
+// element in the slice.
+//
+// See the HTTP header fields specification
+// (http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4) for more details.
+func ResolveAcceptLanguage(req *Request) AcceptLanguages {
+       header := req.Header.Get("Accept-Language")
+       if header == "" {
+               return req.AcceptLanguages
+       }
+
+       acceptLanguageHeaderValues := strings.Split(header, ",")
+       acceptLanguages := make(AcceptLanguages, len(acceptLanguageHeaderValues))
+
+       for i, languageRange := range acceptLanguageHeaderValues {
+               if qualifiedRange := strings.Split(languageRange, ";q="); len(qualifiedRange) == 2 {
+                       quality, err := strconv.ParseFloat(qualifiedRange[1], 32)
+                       if err != nil {
+                               httpLog.Warn("Detected malformed Accept-Language header quality in  assuming quality is 1", "languageRange", languageRange)
+                               acceptLanguages[i] = AcceptLanguage{qualifiedRange[0], 1}
+                       } else {
+                               acceptLanguages[i] = AcceptLanguage{qualifiedRange[0], float32(quality)}
+                       }
+               } else {
+                       acceptLanguages[i] = AcceptLanguage{languageRange, 1}
+               }
+       }
+
+       sort.Sort(acceptLanguages)
+       return acceptLanguages
+}
diff --git a/src/foundation/api/revel/i18n.go b/src/foundation/api/revel/i18n.go
new file mode 100644 (file)
index 0000000..13fe810
--- /dev/null
@@ -0,0 +1,245 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "fmt"
+       "html/template"
+       "os"
+       "path/filepath"
+       "regexp"
+       "strings"
+
+       "github.com/revel/config"
+)
+
+const (
+       // CurrentLocaleViewArg the key for the current locale view arg value
+       CurrentLocaleViewArg = "currentLocale"
+
+       messageFilesDirectory  = "messages"
+       messageFilePattern     = `^\w+\.[a-zA-Z]{2}$`
+       defaultUnknownFormat   = "??? %s ???"
+       unknownFormatConfigKey = "i18n.unknown_format"
+       defaultLanguageOption  = "i18n.default_language"
+       localeCookieConfigKey  = "i18n.cookie"
+)
+
+var (
+       // All currently loaded message configs.
+       messages            map[string]*config.Config
+       localeParameterName string
+       i18nLog             = RevelLog.New("section", "i18n")
+)
+
+// MessageFunc allows you to override the translation interface.
+//
+// Set this to your own function that translates to the current locale.
+// This allows you to set up your own loading and logging of translated texts.
+//
+// See Message(...) in i18n.go for example of function.
+var MessageFunc = Message
+
+// MessageLanguages returns all currently loaded message languages.
+func MessageLanguages() []string {
+       languages := make([]string, len(messages))
+       i := 0
+       for language := range messages {
+               languages[i] = language
+               i++
+       }
+       return languages
+}
+
+// Message performs a message look-up for the given locale and message using the given arguments.
+//
+// When either an unknown locale or message is detected, a specially formatted string is returned.
+func Message(locale, message string, args ...interface{}) string {
+       language, region := parseLocale(locale)
+       unknownValueFormat := getUnknownValueFormat()
+
+       messageConfig, knownLanguage := messages[language]
+       if !knownLanguage {
+               i18nLog.Debugf("Unsupported language for locale '%s' and message '%s', trying default language", locale, message)
+
+               if defaultLanguage, found := Config.String(defaultLanguageOption); found {
+                       i18nLog.Debugf("Using default language '%s'", defaultLanguage)
+
+                       messageConfig, knownLanguage = messages[defaultLanguage]
+                       if !knownLanguage {
+                               i18nLog.Debugf("Unsupported default language for locale '%s' and message '%s'", defaultLanguage, message)
+                               return fmt.Sprintf(unknownValueFormat, message)
+                       }
+               } else {
+                       i18nLog.Warnf("Unable to find default language option (%s); messages for unsupported locales will never be translated", defaultLanguageOption)
+                       return fmt.Sprintf(unknownValueFormat, message)
+               }
+       }
+
+       // This works because unlike the goconfig documentation suggests it will actually
+       // try to resolve message in DEFAULT if it did not find it in the given section.
+       value, err := messageConfig.String(region, message)
+       if err != nil {
+               i18nLog.Warnf("Unknown message '%s' for locale '%s'", message, locale)
+               return fmt.Sprintf(unknownValueFormat, message)
+       }
+
+       if len(args) > 0 {
+               i18nLog.Debugf("Arguments detected, formatting '%s' with %v", value, args)
+               safeArgs := make([]interface{}, 0, len(args))
+               for _, arg := range args {
+                       switch a := arg.(type) {
+                       case template.HTML:
+                               safeArgs = append(safeArgs, a)
+                       case string:
+                               safeArgs = append(safeArgs, template.HTML(template.HTMLEscapeString(a)))
+                       default:
+                               safeArgs = append(safeArgs, a)
+                       }
+               }
+               value = fmt.Sprintf(value, safeArgs...)
+       }
+
+       return value
+}
+
+func parseLocale(locale string) (language, region string) {
+       if strings.Contains(locale, "-") {
+               languageAndRegion := strings.Split(locale, "-")
+               return languageAndRegion[0], languageAndRegion[1]
+       }
+
+       return locale, ""
+}
+
+// Retrieve message format or default format when i18n message is missing.
+func getUnknownValueFormat() string {
+       return Config.StringDefault(unknownFormatConfigKey, defaultUnknownFormat)
+}
+
+// Recursively read and cache all available messages from all message files on the given path.
+func loadMessages(path string) {
+       messages = make(map[string]*config.Config)
+
+       // Read in messages from the modules. Load the module messges first,
+       // so that it can be override in parent application
+       for _, module := range Modules {
+               i18nLog.Debug("Importing messages from module:", "importpath", module.ImportPath)
+               if err := Walk(filepath.Join(module.Path, messageFilesDirectory), loadMessageFile); err != nil &&
+                       !os.IsNotExist(err) {
+                       i18nLog.Error("Error reading messages files from module:", "error", err)
+               }
+       }
+
+       if err := Walk(path, loadMessageFile); err != nil && !os.IsNotExist(err) {
+               i18nLog.Error("Error reading messages files:", "error", err)
+       }
+}
+
+// Load a single message file
+func loadMessageFile(path string, info os.FileInfo, osError error) error {
+       if osError != nil {
+               return osError
+       }
+       if info.IsDir() {
+               return nil
+       }
+
+       if matched, _ := regexp.MatchString(messageFilePattern, info.Name()); matched {
+               messageConfig, err := parseMessagesFile(path)
+               if err != nil {
+                       return err
+               }
+               locale := parseLocaleFromFileName(info.Name())
+
+               // If we have already parsed a message file for this locale, merge both
+               if _, exists := messages[locale]; exists {
+                       messages[locale].Merge(messageConfig)
+                       i18nLog.Debugf("Successfully merged messages for locale '%s'", locale)
+               } else {
+                       messages[locale] = messageConfig
+               }
+
+               i18nLog.Debug("Successfully loaded messages from file", "file", info.Name())
+       } else {
+               i18nLog.Warn("Ignoring file because it did not have a valid extension", "file", info.Name())
+       }
+
+       return nil
+}
+
+func parseMessagesFile(path string) (messageConfig *config.Config, err error) {
+       messageConfig, err = config.ReadDefault(path)
+       return
+}
+
+func parseLocaleFromFileName(file string) string {
+       extension := filepath.Ext(file)[1:]
+       return strings.ToLower(extension)
+}
+
+func init() {
+       OnAppStart(func() {
+               loadMessages(filepath.Join(BasePath, messageFilesDirectory))
+               localeParameterName = Config.StringDefault("i18n.locale.parameter", "")
+       }, 0)
+}
+
+func I18nFilter(c *Controller, fc []Filter) {
+       foundLocale := false
+       // Search for a parameter first
+       if localeParameterName != "" {
+               if locale, found := c.Params.Values[localeParameterName]; found && len(locale[0]) > 0 {
+                       setCurrentLocaleControllerArguments(c, locale[0])
+                       foundLocale = true
+                       i18nLog.Debug("Found locale parameter value: ", "locale", locale[0])
+               }
+       }
+       if !foundLocale {
+               if foundCookie, cookieValue := hasLocaleCookie(c.Request); foundCookie {
+                       i18nLog.Debug("Found locale cookie value: ", "cookie", cookieValue)
+                       setCurrentLocaleControllerArguments(c, cookieValue)
+               } else if foundHeader, headerValue := hasAcceptLanguageHeader(c.Request); foundHeader {
+                       i18nLog.Debug("Found Accept-Language header value: ", "header", headerValue)
+                       setCurrentLocaleControllerArguments(c, headerValue)
+               } else {
+                       i18nLog.Debug("Unable to find locale in cookie or header, using empty string")
+                       setCurrentLocaleControllerArguments(c, "")
+               }
+       }
+       fc[0](c, fc[1:])
+}
+
+// Set the current locale controller argument (CurrentLocaleControllerArg) with the given locale.
+func setCurrentLocaleControllerArguments(c *Controller, locale string) {
+       c.Request.Locale = locale
+       c.ViewArgs[CurrentLocaleViewArg] = locale
+}
+
+// Determine whether the given request has valid Accept-Language value.
+//
+// Assumes that the accept languages stored in the request are sorted according to quality, with top
+// quality first in the slice.
+func hasAcceptLanguageHeader(request *Request) (bool, string) {
+       if request.AcceptLanguages != nil && len(request.AcceptLanguages) > 0 {
+               return true, request.AcceptLanguages[0].Language
+       }
+
+       return false, ""
+}
+
+// Determine whether the given request has a valid language cookie value.
+func hasLocaleCookie(request *Request) (bool, string) {
+       if request != nil {
+               name := Config.StringDefault(localeCookieConfigKey, CookiePrefix+"_LANG")
+               cookie, err := request.Cookie(name)
+               if err == nil {
+                       return true, cookie.GetValue()
+               }
+               i18nLog.Debug("Unable to read locale cookie ", "name", name, "error", err)
+       }
+
+       return false, ""
+}
diff --git a/src/foundation/api/revel/i18n_test.go b/src/foundation/api/revel/i18n_test.go
new file mode 100644 (file)
index 0000000..ce2f675
--- /dev/null
@@ -0,0 +1,273 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "html/template"
+       "net/http"
+       "testing"
+       "time"
+
+       "github.com/revel/config"
+       "github.com/revel/revel/logger"
+)
+
+const (
+       testDataPath   string = "testdata/i18n"
+       testConfigPath string = "testdata/i18n/config"
+       testConfigName string = "test_app.conf"
+)
+
+func TestI18nLoadMessages(t *testing.T) {
+       loadMessages(testDataPath)
+
+       // Assert that we have the expected number of languages
+       if len(MessageLanguages()) != 2 {
+               t.Fatalf("Expected messages to contain no more or less than 2 languages, instead there are %d languages", len(MessageLanguages()))
+       }
+}
+
+func TestI18nMessage(t *testing.T) {
+       loadMessages(testDataPath)
+       loadTestI18nConfig(t)
+
+       // Assert that we can get a message and we get the expected return value
+       if message := Message("nl", "greeting"); message != "Hallo" {
+               t.Errorf("Message 'greeting' for locale 'nl' (%s) does not have the expected value", message)
+       }
+       if message := Message("en", "greeting"); message != "Hello" {
+               t.Errorf("Message 'greeting' for locale 'en' (%s) does not have the expected value", message)
+       }
+       if message := Message("en", "folded"); message != "Greeting is 'Hello'" {
+               t.Error("Unexpected unfolded message: ", message)
+       }
+       if message := Message("en", "arguments.string", "Vincent Hanna"); message != "My name is Vincent Hanna" {
+               t.Errorf("Message 'arguments.string' for locale 'en' (%s) does not have the expected value", message)
+       }
+       if message := Message("en", "arguments.hex", 1234567, 1234567); message != "The number 1234567 in hexadecimal notation would be 12d687" {
+               t.Errorf("Message 'arguments.hex' for locale 'en' (%s) does not have the expected value", message)
+       }
+       if message := Message("en", "folded.arguments", 12345); message != "Rob is 12345 years old" {
+               t.Errorf("Message 'folded.arguments' for locale 'en' (%s) does not have the expected value", message)
+       }
+       if message := Message("en-AU", "greeting"); message != "G'day" {
+               t.Errorf("Message 'greeting' for locale 'en-AU' (%s) does not have the expected value", message)
+       }
+       if message := Message("en-AU", "only_exists_in_default"); message != "Default" {
+               t.Errorf("Message 'only_exists_in_default' for locale 'en-AU' (%s) does not have the expected value", message)
+       }
+
+       // Assert that message merging works
+       if message := Message("en", "greeting2"); message != "Yo!" {
+               t.Errorf("Message 'greeting2' for locale 'en' (%s) does not have the expected value", message)
+       }
+
+       // Assert that we get the expected return value for a locale that doesn't exist
+       if message := Message("unknown locale", "message"); message != "??? message ???" {
+               t.Error("Locale 'unknown locale' is not supposed to exist")
+       }
+       // Assert that we get the expected return value for a message that doesn't exist
+       if message := Message("nl", "unknown message"); message != "??? unknown message ???" {
+               t.Error("Message 'unknown message' is not supposed to exist")
+       }
+       // XSS
+       if message := Message("en", "arguments.string", "<img src=a onerror=alert(1) />"); message != "My name is &lt;img src=a onerror=alert(1) /&gt;" {
+               t.Error("XSS protection for messages is broken:", message)
+       }
+       // Avoid escaping HTML
+       if message := Message("en", "arguments.string", template.HTML("<img src=a onerror=alert(1) />")); message != "My name is <img src=a onerror=alert(1) />" {
+               t.Error("Passing safe HTML to message is broken:", message)
+       }
+}
+
+func TestI18nMessageWithDefaultLocale(t *testing.T) {
+       loadMessages(testDataPath)
+       loadTestI18nConfig(t)
+
+       if message := Message("doesn't exist", "greeting"); message != "Hello" {
+               t.Errorf("Expected message '%s' for unknown locale to be default '%s' but was '%s'", "greeting", "Hello", message)
+       }
+       if message := Message("doesn't exist", "unknown message"); message != "??? unknown message ???" {
+               t.Error("Message 'unknown message' is not supposed to exist in the default language")
+       }
+}
+
+func TestHasLocaleCookie(t *testing.T) {
+       loadTestI18nConfig(t)
+
+       if found, value := hasLocaleCookie(buildRequestWithCookie("APP_LANG", "en").Request); !found {
+               t.Errorf("Expected %s cookie with value '%s' but found nothing or unexpected value '%s'", "APP_LANG", "en", value)
+       }
+       if found, value := hasLocaleCookie(buildRequestWithCookie("APP_LANG", "en-US").Request); !found {
+               t.Errorf("Expected %s cookie with value '%s' but found nothing or unexpected value '%s'", "APP_LANG", "en-US", value)
+       }
+       if found, _ := hasLocaleCookie(buildRequestWithCookie("DOESNT_EXIST", "en-US").Request); found {
+               t.Errorf("Expected %s cookie to not exist, but apparently it does", "DOESNT_EXIST")
+       }
+}
+
+func TestHasLocaleCookieWithInvalidConfig(t *testing.T) {
+       loadTestI18nConfigWithoutLanguageCookieOption(t)
+       if found, _ := hasLocaleCookie(buildRequestWithCookie("APP_LANG", "en-US").Request); found {
+               t.Errorf("Expected %s cookie to not exist because the configured name is missing", "APP_LANG")
+       }
+       if found, _ := hasLocaleCookie(buildRequestWithCookie("REVEL_LANG", "en-US").Request); !found {
+               t.Errorf("Expected %s cookie to exist", "REVEL_LANG")
+       }
+}
+
+func TestHasAcceptLanguageHeader(t *testing.T) {
+       if found, value := hasAcceptLanguageHeader(buildRequestWithAcceptLanguages("en-US").Request); !found && value != "en-US" {
+               t.Errorf("Expected to find Accept-Language header with value '%s', found '%s' instead", "en-US", value)
+       }
+       if found, value := hasAcceptLanguageHeader(buildRequestWithAcceptLanguages("en-GB", "en-US", "nl").Request); !found && value != "en-GB" {
+               t.Errorf("Expected to find Accept-Language header with value '%s', found '%s' instead", "en-GB", value)
+       }
+}
+
+func TestBeforeRequest(t *testing.T) {
+       loadTestI18nConfig(t)
+
+       c := buildEmptyRequest()
+       if I18nFilter(c, NilChain); c.Request.Locale != "" {
+               t.Errorf("Expected to find current language '%s' in controller, found '%s' instead", "", c.Request.Locale)
+       }
+
+       c = buildRequestWithCookie("APP_LANG", "en-US")
+       if I18nFilter(c, NilChain); c.Request.Locale != "en-US" {
+               t.Errorf("Expected to find current language '%s' in controller, found '%s' instead", "en-US", c.Request.Locale)
+       }
+
+       c = buildRequestWithAcceptLanguages("en-GB", "en-US")
+       if I18nFilter(c, NilChain); c.Request.Locale != "en-GB" {
+               t.Errorf("Expected to find current language '%s' in controller, found '%s' instead", "en-GB", c.Request.Locale)
+       }
+}
+
+func TestI18nMessageUnknownValueFormat(t *testing.T) {
+       loadMessages(testDataPath)
+       loadTestI18nConfigWithUnknowFormatOption(t)
+
+       // Assert that we can get a message and we get the expected return value
+       if message := Message("en", "greeting"); message != "Hello" {
+               t.Errorf("Message 'greeting' for locale 'en' (%s) does not have the expected value", message)
+       }
+
+       // Assert that we get the expected return value with original format
+       if message := Message("unknown locale", "message"); message != "*** message ***" {
+               t.Error("Locale 'unknown locale' is not supposed to exist")
+       }
+       if message := Message("nl", "unknown message"); message != "*** unknown message ***" {
+               t.Error("Message 'unknown message' is not supposed to exist")
+       }
+}
+
+func BenchmarkI18nLoadMessages(b *testing.B) {
+       excludeFromTimer(b, func() {
+               RevelLog.SetHandler(logger.FuncHandler(func(r *logger.Record) error {
+                       return nil
+               }))
+       })
+
+       for i := 0; i < b.N; i++ {
+               loadMessages(testDataPath)
+       }
+}
+
+func BenchmarkI18nMessage(b *testing.B) {
+       for i := 0; i < b.N; i++ {
+               Message("nl", "greeting")
+       }
+}
+
+func BenchmarkI18nMessageWithArguments(b *testing.B) {
+       excludeFromTimer(b, func() {
+               RevelLog.SetHandler(logger.FuncHandler(func(r *logger.Record) error {
+                       return nil
+               }))
+       })
+
+
+       for i := 0; i < b.N; i++ {
+               Message("en", "arguments.string", "Vincent Hanna")
+       }
+}
+
+func BenchmarkI18nMessageWithFoldingAndArguments(b *testing.B) {
+       excludeFromTimer(b, func() {
+               RevelLog.SetHandler(logger.FuncHandler(func(r *logger.Record) error {
+                       return nil
+               }))
+       })
+
+
+       for i := 0; i < b.N; i++ {
+               Message("en", "folded.arguments", 12345)
+       }
+}
+
+// Exclude whatever operations the given function performs from the benchmark timer.
+func excludeFromTimer(b *testing.B, f func()) {
+       b.StopTimer()
+       f()
+       b.StartTimer()
+}
+
+func loadTestI18nConfig(t *testing.T) {
+       ConfPaths = append(ConfPaths, testConfigPath)
+       testConfig, err := config.LoadContext(testConfigName, ConfPaths)
+       if err != nil {
+               t.Fatalf("Unable to load test config '%s': %s", testConfigName, err.Error())
+       }
+       Config = testConfig
+       CookiePrefix = Config.StringDefault("cookie.prefix", "REVEL")
+}
+
+func loadTestI18nConfigWithoutLanguageCookieOption(t *testing.T) {
+       loadTestI18nConfig(t)
+       Config.Raw().RemoveOption("DEFAULT", "i18n.cookie")
+}
+
+func loadTestI18nConfigWithUnknowFormatOption(t *testing.T) {
+       loadTestI18nConfig(t)
+       Config.Raw().AddOption("DEFAULT", "i18n.unknown_format", "*** %s ***")
+}
+
+func buildRequestWithCookie(name, value string) *Controller {
+       httpRequest, _ := http.NewRequest("GET", "/", nil)
+       controller := NewTestController(nil, httpRequest)
+
+       httpRequest.AddCookie(&http.Cookie{
+               Name:       name,
+               Value:      value,
+               Path:       "",
+               Domain:     "",
+               Expires:    time.Now(),
+               RawExpires: "",
+               MaxAge:     0,
+               Secure:     false,
+               HttpOnly:   false,
+               Raw:        "",
+               Unparsed:   nil,
+       })
+       return controller
+}
+
+func buildRequestWithAcceptLanguages(acceptLanguages ...string) *Controller {
+       httpRequest, _ := http.NewRequest("GET", "/", nil)
+       controller := NewTestController(nil, httpRequest)
+
+       request := controller.Request
+       for _, acceptLanguage := range acceptLanguages {
+               request.AcceptLanguages = append(request.AcceptLanguages, AcceptLanguage{acceptLanguage, 1})
+       }
+       return controller
+}
+
+func buildEmptyRequest() *Controller {
+       httpRequest, _ := http.NewRequest("GET", "/", nil)
+       controller := NewTestController(nil, httpRequest)
+       return controller
+}
diff --git a/src/foundation/api/revel/intercept.go b/src/foundation/api/revel/intercept.go
new file mode 100644 (file)
index 0000000..2ee8677
--- /dev/null
@@ -0,0 +1,249 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "log"
+       "reflect"
+       "sort"
+)
+
+// An InterceptorFunc is functionality invoked by the framework BEFORE or AFTER
+// an action.
+//
+// An interceptor may optionally return a Result (instead of nil).  Depending on
+// when the interceptor was invoked, the response is different:
+// 1. BEFORE:  No further interceptors are invoked, and neither is the action.
+// 2. AFTER: Further interceptors are still run.
+// In all cases, any returned Result will take the place of any existing Result.
+//
+// In the BEFORE case, that returned Result is guaranteed to be final, while
+// in the AFTER case it is possible that a further interceptor could emit its
+// own Result.
+//
+// Interceptors are called in the order that they are added.
+//
+// ***
+//
+// Two types of interceptors are provided: Funcs and Methods
+//
+// Func Interceptors may apply to any / all Controllers.
+//
+//   func example(*revel.Controller) revel.Result
+//
+// Method Interceptors are provided so that properties can be set on application
+// controllers.
+//
+//   func (c AppController) example() revel.Result
+//   func (c *AppController) example() revel.Result
+//
+type InterceptorFunc func(*Controller) Result
+type InterceptorMethod interface{}
+type When int
+
+const (
+       BEFORE When = iota
+       AFTER
+       PANIC
+       FINALLY
+)
+
+type InterceptTarget int
+
+const (
+       AllControllers InterceptTarget = iota
+)
+
+type Interception struct {
+       When When
+
+       function InterceptorFunc
+       method   InterceptorMethod
+
+       callable     reflect.Value
+       target       reflect.Type
+       interceptAll bool
+}
+
+// Invoke performs the given interception.
+// val is a pointer to the App Controller.
+func (i Interception) Invoke(val reflect.Value, target *reflect.Value) reflect.Value {
+       var arg reflect.Value
+       if i.function == nil {
+               // If it's an InterceptorMethod, then we have to pass in the target type.
+               arg = *target
+       } else {
+               // If it's an InterceptorFunc, then the type must be *Controller.
+               // We can find that by following the embedded types up the chain.
+               for val.Type() != controllerPtrType {
+                       if val.Kind() == reflect.Ptr {
+                               val = val.Elem()
+                       }
+                       val = val.Field(0)
+               }
+               arg = val
+       }
+
+       vals := i.callable.Call([]reflect.Value{arg})
+       return vals[0]
+}
+
+func InterceptorFilter(c *Controller, fc []Filter) {
+       defer invokeInterceptors(FINALLY, c)
+       defer func() {
+               if err := recover(); err != nil {
+                       invokeInterceptors(PANIC, c)
+                       panic(err)
+               }
+       }()
+
+       // Invoke the BEFORE interceptors and return early, if we get a result.
+       invokeInterceptors(BEFORE, c)
+       if c.Result != nil {
+               return
+       }
+
+       fc[0](c, fc[1:])
+       invokeInterceptors(AFTER, c)
+}
+
+func invokeInterceptors(when When, c *Controller) {
+       var (
+               app    = reflect.ValueOf(c.AppController)
+               result Result
+       )
+
+       for _, intc := range getInterceptors(when, app) {
+               resultValue := intc.Interceptor.Invoke(app, &intc.Target)
+               if !resultValue.IsNil() {
+                       result = resultValue.Interface().(Result)
+               }
+               if when == BEFORE && result != nil {
+                       c.Result = result
+                       return
+               }
+       }
+       if result != nil {
+               c.Result = result
+       }
+}
+
+var interceptors []*Interception
+
+// InterceptFunc installs a general interceptor.
+// This can be applied to any Controller.
+// It must have the signature of:
+//   func example(c *revel.Controller) revel.Result
+func InterceptFunc(intc InterceptorFunc, when When, target interface{}) {
+       interceptors = append(interceptors, &Interception{
+               When:         when,
+               function:     intc,
+               callable:     reflect.ValueOf(intc),
+               target:       reflect.TypeOf(target),
+               interceptAll: target == AllControllers,
+       })
+}
+
+// InterceptMethod installs an interceptor method that applies to its own Controller.
+//   func (c AppController) example() revel.Result
+//   func (c *AppController) example() revel.Result
+func InterceptMethod(intc InterceptorMethod, when When) {
+       methodType := reflect.TypeOf(intc)
+       if methodType.Kind() != reflect.Func || methodType.NumOut() != 1 || methodType.NumIn() != 1 {
+               log.Fatalln("Interceptor method should have signature like",
+                       "'func (c *AppController) example() revel.Result' but was", methodType)
+       }
+
+       interceptors = append(interceptors, &Interception{
+               When:     when,
+               method:   intc,
+               callable: reflect.ValueOf(intc),
+               target:   methodType.In(0),
+       })
+}
+
+// This item is used to provide a sortable set to be returned to the caller. This ensures calls order is maintained
+//
+type interceptorItem struct {
+       Interceptor *Interception
+       Target      reflect.Value
+       Level       int
+}
+type interceptorItemList []*interceptorItem
+
+func (a interceptorItemList) Len() int           { return len(a) }
+func (a interceptorItemList) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
+func (a interceptorItemList) Less(i, j int) bool { return a[i].Level < a[j].Level }
+
+type reverseInterceptorItemList []*interceptorItem
+
+func (a reverseInterceptorItemList) Len() int           { return len(a) }
+func (a reverseInterceptorItemList) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
+func (a reverseInterceptorItemList) Less(i, j int) bool { return a[i].Level > a[j].Level }
+func getInterceptors(when When, val reflect.Value) interceptorItemList {
+       result := interceptorItemList{}
+       for _, intc := range interceptors {
+               if intc.When != when {
+                       continue
+               }
+
+               level, target := findTarget(val, intc.target)
+               if intc.interceptAll || target.IsValid() {
+                       result = append(result, &interceptorItem{intc, target, level})
+               }
+       }
+
+       // Before is deepest to highest
+       if when == BEFORE {
+               sort.Sort(result)
+       } else {
+               // Everything else is highest to deepest
+               sort.Sort(reverseInterceptorItemList(result))
+       }
+       return result
+}
+
+// Find the value of the target, starting from val and including embedded types.
+// Also, convert between any difference in indirection.
+// If the target couldn't be found, the returned Value will have IsValid() == false
+func findTarget(val reflect.Value, target reflect.Type) (int, reflect.Value) {
+       // Look through the embedded types (until we reach the *revel.Controller at the top).
+       valueQueue := []reflect.Value{val}
+       level := 0
+       for len(valueQueue) > 0 {
+               val, valueQueue = valueQueue[0], valueQueue[1:]
+
+               // Check if val is of a similar type to the target type.
+               if val.Type() == target {
+                       return level, val
+               }
+               if val.Kind() == reflect.Ptr && val.Elem().Type() == target {
+                       return level, val.Elem()
+               }
+               if target.Kind() == reflect.Ptr && target.Elem() == val.Type() {
+                       return level, val.Addr()
+               }
+
+               // If we reached the *revel.Controller and still didn't find what we were
+               // looking for, give up.
+               if val.Type() == controllerPtrType {
+                       continue
+               }
+
+               // Else, add each anonymous field to the queue.
+               if val.Kind() == reflect.Ptr {
+                       val = val.Elem()
+               }
+
+               for i := 0; i < val.NumField(); i++ {
+                       if val.Type().Field(i).Anonymous {
+                               valueQueue = append(valueQueue, val.Field(i))
+                       }
+               }
+               level--
+       }
+
+       return level, reflect.Value{}
+}
diff --git a/src/foundation/api/revel/intercept_test.go b/src/foundation/api/revel/intercept_test.go
new file mode 100644 (file)
index 0000000..ee2138d
--- /dev/null
@@ -0,0 +1,85 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "reflect"
+       "testing"
+)
+
+var funcP = func(c *Controller) Result { return nil }
+var funcP2 = func(c *Controller) Result { return nil }
+
+type InterceptController struct{ *Controller }
+type InterceptControllerN struct{ InterceptController }
+type InterceptControllerP struct{ *InterceptController }
+type InterceptControllerNP struct {
+       *Controller
+       InterceptControllerN
+       InterceptControllerP
+}
+
+func (c InterceptController) methN() Result  { return nil }
+func (c *InterceptController) methP() Result { return nil }
+
+func (c InterceptControllerN) methNN() Result  { return nil }
+func (c *InterceptControllerN) methNP() Result { return nil }
+func (c InterceptControllerP) methPN() Result  { return nil }
+func (c *InterceptControllerP) methPP() Result { return nil }
+
+// Methods accessible from InterceptControllerN
+var MethodN = []interface{}{
+       InterceptController.methN,
+       (*InterceptController).methP,
+       InterceptControllerN.methNN,
+       (*InterceptControllerN).methNP,
+}
+
+// Methods accessible from InterceptControllerP
+var MethodP = []interface{}{
+       InterceptController.methN,
+       (*InterceptController).methP,
+       InterceptControllerP.methPN,
+       (*InterceptControllerP).methPP,
+}
+
+// This checks that all the various kinds of interceptor functions/methods are
+// properly invoked.
+func TestInvokeArgType(t *testing.T) {
+       n := InterceptControllerN{InterceptController{&Controller{}}}
+       p := InterceptControllerP{&InterceptController{&Controller{}}}
+       np := InterceptControllerNP{&Controller{}, n, p}
+       testInterceptorController(t, reflect.ValueOf(&n), MethodN)
+       testInterceptorController(t, reflect.ValueOf(&p), MethodP)
+       testInterceptorController(t, reflect.ValueOf(&np), MethodN)
+       testInterceptorController(t, reflect.ValueOf(&np), MethodP)
+}
+
+func testInterceptorController(t *testing.T, appControllerPtr reflect.Value, methods []interface{}) {
+       interceptors = []*Interception{}
+       InterceptFunc(funcP, BEFORE, appControllerPtr.Elem().Interface())
+       InterceptFunc(funcP2, BEFORE, AllControllers)
+       for _, m := range methods {
+               InterceptMethod(m, BEFORE)
+       }
+       ints := getInterceptors(BEFORE, appControllerPtr)
+
+       if len(ints) != 6 {
+               t.Fatalf("N: Expected 6 interceptors, got %d.", len(ints))
+       }
+
+       testInterception(t, ints[0], reflect.ValueOf(&Controller{}))
+       testInterception(t, ints[1], reflect.ValueOf(&Controller{}))
+       for i := range methods {
+               testInterception(t, ints[i+2], appControllerPtr)
+       }
+}
+
+func testInterception(t *testing.T, intc *interceptorItem, arg reflect.Value) {
+       val := intc.Interceptor.Invoke(arg, &intc.Target)
+       if !val.IsNil() {
+               t.Errorf("Failed (%v): Expected nil got %v", intc, val)
+       }
+}
diff --git a/src/foundation/api/revel/invoker.go b/src/foundation/api/revel/invoker.go
new file mode 100644 (file)
index 0000000..24d40fa
--- /dev/null
@@ -0,0 +1,50 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "io"
+       "reflect"
+)
+
+var (
+       controllerPtrType = reflect.TypeOf(&Controller{})
+       websocketType     = reflect.TypeOf((*ServerWebSocket)(nil)).Elem()
+)
+
+func ActionInvoker(c *Controller, _ []Filter) {
+       // Instantiate the method.
+       methodValue := reflect.ValueOf(c.AppController).MethodByName(c.MethodType.Name)
+
+       // Collect the values for the method's arguments.
+       var methodArgs []reflect.Value
+       for _, arg := range c.MethodType.Args {
+               // If they accept a websocket connection, treat that arg specially.
+               var boundArg reflect.Value
+               if arg.Type.Implements(websocketType) {
+                       boundArg = reflect.ValueOf(c.Request.WebSocket)
+               } else {
+                       boundArg = Bind(c.Params, arg.Name, arg.Type)
+                       // #756 - If the argument is a closer, defer a Close call,
+                       // so we don't risk on leaks.
+                       if closer, ok := boundArg.Interface().(io.Closer); ok {
+                               defer func() {
+                                       _ = closer.Close()
+                               }()
+                       }
+               }
+               methodArgs = append(methodArgs, boundArg)
+       }
+
+       var resultValue reflect.Value
+       if methodValue.Type().IsVariadic() {
+               resultValue = methodValue.CallSlice(methodArgs)[0]
+       } else {
+               resultValue = methodValue.Call(methodArgs)[0]
+       }
+       if resultValue.Kind() == reflect.Interface && !resultValue.IsNil() {
+               c.Result = resultValue.Interface().(Result)
+       }
+}
diff --git a/src/foundation/api/revel/invoker_test.go b/src/foundation/api/revel/invoker_test.go
new file mode 100644 (file)
index 0000000..dff94cc
--- /dev/null
@@ -0,0 +1,134 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "net/url"
+       "reflect"
+       "testing"
+)
+
+// These tests verify that Controllers are initialized properly, given the range
+// of embedding possibilities..
+
+type P struct{ *Controller }
+
+type PN struct{ P }
+
+type PNN struct{ PN }
+
+// Embedded via two paths
+type P2 struct{ *Controller }
+type PP2 struct {
+       *Controller // Need to embed this explicitly to avoid duplicate selector.
+       P
+       P2
+       PNN
+}
+
+func TestFindControllers(t *testing.T) {
+       controllers = make(map[string]*ControllerType)
+       RegisterController((*P)(nil), nil)
+       RegisterController((*PN)(nil), nil)
+       RegisterController((*PNN)(nil), nil)
+       RegisterController((*PP2)(nil), nil)
+
+       // Test construction of indexes to each *Controller
+       checkSearchResults(t, P{}, [][]int{{0}})
+       checkSearchResults(t, PN{}, [][]int{{0, 0}})
+       checkSearchResults(t, PNN{}, [][]int{{0, 0, 0}})
+       checkSearchResults(t, PP2{}, [][]int{{0}, {1, 0}, {2, 0}, {3, 0, 0, 0}})
+}
+
+func checkSearchResults(t *testing.T, obj interface{}, expected [][]int) {
+       actual := findControllers(reflect.TypeOf(obj))
+       if !reflect.DeepEqual(expected, actual) {
+               t.Errorf("Indexes do not match.  expected %v actual %v", expected, actual)
+       }
+}
+
+func TestSetAction(t *testing.T) {
+       controllers = make(map[string]*ControllerType)
+       RegisterController((*P)(nil), []*MethodType{{Name: "Method"}})
+       RegisterController((*PNN)(nil), []*MethodType{{Name: "Method"}})
+       RegisterController((*PP2)(nil), []*MethodType{{Name: "Method"}})
+
+       // Test that all *revel.Controllers are initialized.
+       c := &Controller{Name: "Test"}
+       if err := c.SetAction("P", "Method"); err != nil {
+               t.Error(err)
+       } else if c.AppController.(*P).Controller != c {
+               t.Errorf("P not initialized")
+       }
+
+       if err := c.SetAction("PNN", "Method"); err != nil {
+               t.Error(err)
+       } else if c.AppController.(*PNN).Controller != c {
+               t.Errorf("PNN not initialized")
+       }
+
+       // PP2 has 4 different slots for *Controller.
+       if err := c.SetAction("PP2", "Method"); err != nil {
+               t.Error(err)
+       } else if pp2 := c.AppController.(*PP2); pp2.Controller != c ||
+               pp2.P.Controller != c ||
+               pp2.P2.Controller != c ||
+               pp2.PNN.Controller != c {
+               t.Errorf("PP2 not initialized")
+       }
+}
+
+func BenchmarkSetAction(b *testing.B) {
+       type Mixin1 struct {
+               *Controller
+               x, y int
+               foo  string
+       }
+       type Mixin2 struct {
+               *Controller
+               a, b float64
+               bar  string
+       }
+
+       type Benchmark struct {
+               *Controller
+               Mixin1
+               Mixin2
+               user interface{}
+               guy  string
+       }
+
+       RegisterController((*Mixin1)(nil), []*MethodType{{Name: "Method"}})
+       RegisterController((*Mixin2)(nil), []*MethodType{{Name: "Method"}})
+       RegisterController((*Benchmark)(nil), []*MethodType{{Name: "Method"}})
+       c := Controller{
+               ViewArgs: make(map[string]interface{}),
+       }
+
+       for i := 0; i < b.N; i++ {
+               if err := c.SetAction("Benchmark", "Method"); err != nil {
+                       b.Errorf("Failed to set action: %s", err)
+                       return
+               }
+       }
+}
+
+func BenchmarkInvoker(b *testing.B) {
+       startFakeBookingApp()
+       c := NewTestController(nil, showRequest)
+       c.ViewArgs = make(map[string]interface{})
+       if err := c.SetAction("Hotels", "Show"); err != nil {
+               b.Errorf("Failed to set action: %s", err)
+               return
+       }
+
+       c.Params = &Params{Values: make(url.Values)}
+       c.Params.Set("id", "3")
+
+       b.ResetTimer()
+       for i := 0; i < b.N; i++ {
+               ActionInvoker(c, nil)
+       }
+}
diff --git a/src/foundation/api/revel/libs.go b/src/foundation/api/revel/libs.go
new file mode 100644 (file)
index 0000000..fb17178
--- /dev/null
@@ -0,0 +1,86 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "crypto/hmac"
+       "crypto/sha1"
+       "encoding/hex"
+       "io"
+       "reflect"
+       "strings"
+)
+
+// Sign a given string with the app-configured secret key.
+// If no secret key is set, returns the empty string.
+// Return the signature in base64 (URLEncoding).
+func Sign(message string) string {
+       if len(secretKey) == 0 {
+               return ""
+       }
+       mac := hmac.New(sha1.New, secretKey)
+       if _, err := io.WriteString(mac, message); err != nil {
+               utilLog.Error("WriteString failed", "error", err)
+               return ""
+       }
+       return hex.EncodeToString(mac.Sum(nil))
+}
+
+// Verify returns true if the given signature is correct for the given message.
+// e.g. it matches what we generate with Sign()
+func Verify(message, sig string) bool {
+       return hmac.Equal([]byte(sig), []byte(Sign(message)))
+}
+
+// ToBool method converts/assert value into true or false. Default is true.
+// When converting to boolean, the following values are considered FALSE:
+// - The integer value is 0 (zero)
+// - The float value 0.0 (zero)
+// - The complex value 0.0 (zero)
+// - For string value, please refer `revel.Atob` method
+// - An array, map, slice with zero elements
+// - Boolean value returned as-is
+// - "nil" value
+func ToBool(val interface{}) bool {
+       if val == nil {
+               return false
+       }
+
+       v := reflect.ValueOf(val)
+       switch v.Kind() {
+       case reflect.Bool:
+               return v.Bool()
+       case reflect.String:
+               return Atob(v.String())
+       case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+               return v.Int() != 0
+       case reflect.Float32, reflect.Float64:
+               return v.Float() != 0.0
+       case reflect.Complex64, reflect.Complex128:
+               return v.Complex() != 0.0
+       case reflect.Array, reflect.Map, reflect.Slice:
+               return v.Len() != 0
+       }
+
+       // Return true by default
+       return true
+}
+
+// Atob converts string into boolean. It is in-case sensitive
+// When converting to boolean, the following values are considered FALSE:
+// - The "" (empty) string
+// - The "false" string
+// - The "f" string
+// - The "off" string,
+// - The string "0" & "0.0"
+func Atob(v string) bool {
+       switch strings.TrimSpace(strings.ToLower(v)) {
+       case "", "false", "off", "f", "0", "0.0":
+               return false
+       }
+
+       // Return true by default
+       return true
+}
diff --git a/src/foundation/api/revel/libs_test.go b/src/foundation/api/revel/libs_test.go
new file mode 100644 (file)
index 0000000..d8f8355
--- /dev/null
@@ -0,0 +1,38 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import "testing"
+
+func TestToBooleanForFalse(t *testing.T) {
+       if ToBool(nil) ||
+               ToBool([]string{}) ||
+               ToBool(map[string]string{}) ||
+               ToBool(0) ||
+               ToBool(0.0) ||
+               ToBool("") ||
+               ToBool("false") ||
+               ToBool("0") ||
+               ToBool("0.0") ||
+               ToBool("off") ||
+               ToBool("f") {
+               t.Error("Expected 'false' got 'true'")
+       }
+}
+
+func TestToBooleanForTrue(t *testing.T) {
+       if !ToBool([]string{"true"}) ||
+               !ToBool(map[string]string{"true": "value"}) ||
+               !ToBool(1) ||
+               !ToBool(0.1) ||
+               !ToBool("not empty") ||
+               !ToBool("true") ||
+               !ToBool("1") ||
+               !ToBool("1.0") ||
+               !ToBool("on") ||
+               !ToBool("t") {
+               t.Error("Expected 'true' got 'false'")
+       }
+}
diff --git a/src/foundation/api/revel/logger.go b/src/foundation/api/revel/logger.go
new file mode 100644 (file)
index 0000000..7e928f2
--- /dev/null
@@ -0,0 +1,65 @@
+package revel
+
+import (
+       "github.com/revel/revel/logger"
+)
+
+//Logger
+var (
+       // The root log is what all other logs are branched from, meaning if you set the handler for the root
+       // it will adjust all children
+       RootLog = logger.New()
+       // This logger is the application logger, use this for your application log messages - ie jobs and startup,
+       // Use Controller.Log for Controller logging
+       // The requests are logged to this logger with the context of `section:requestlog`
+       AppLog = RootLog.New("module", "app")
+       // This is the logger revel writes to, added log messages will have a context of module:revel in them
+       // It is based off of `RootLog`
+       RevelLog = RootLog.New("module", "revel")
+
+       // This is the handler for the AppLog, it is stored so that if the AppLog is changed it can be assigned to the
+       // new AppLog
+       appLogHandler *logger.CompositeMultiHandler
+
+       // This oldLog is the revel logger, historical for revel, The application should use the AppLog or the Controller.oldLog
+       // DEPRECATED
+       oldLog = AppLog.New("section", "deprecated")
+       // System logger
+       SysLog = AppLog.New("section", "system")
+)
+
+// Initialize the loggers first
+func init() {
+
+       //RootLog.SetHandler(
+       //      logger.LevelHandler(logger.LogLevel(log15.LvlDebug),
+       //              logger.StreamHandler(os.Stdout, logger.TerminalFormatHandler(false, true))))
+       initLoggers()
+       OnAppStart(initLoggers, -5)
+
+}
+func initLoggers() {
+       appHandle := logger.InitializeFromConfig(BasePath, Config)
+
+       // Set all the log handlers
+       setAppLog(AppLog, appHandle)
+}
+
+// Set the application log and handler, if handler is nil it will
+// use the same handler used to configure the application log before
+func setAppLog(appLog logger.MultiLogger, appHandler *logger.CompositeMultiHandler) {
+       if appLog != nil {
+               AppLog = appLog
+       }
+       if appHandler != nil {
+               appLogHandler = appHandler
+               // Set the app log and the handler for all forked loggers
+               RootLog.SetHandler(appLogHandler)
+
+               // Set the system log handler - this sets golang writer stream to the
+               // sysLog router
+               logger.SetDefaultLog(SysLog)
+               SysLog.SetStackDepth(5)
+               SysLog.SetHandler(appLogHandler)
+       }
+}
diff --git a/src/foundation/api/revel/logger/composite_multihandler.go b/src/foundation/api/revel/logger/composite_multihandler.go
new file mode 100644 (file)
index 0000000..fb15e8a
--- /dev/null
@@ -0,0 +1,174 @@
+package logger
+
+import (
+       "github.com/mattn/go-colorable"
+       "gopkg.in/natefinch/lumberjack.v2"
+       "io"
+       "os"
+)
+
+type CompositeMultiHandler struct {
+       DebugHandler    LogHandler
+       InfoHandler     LogHandler
+       WarnHandler     LogHandler
+       ErrorHandler    LogHandler
+       CriticalHandler LogHandler
+}
+
+func NewCompositeMultiHandler() (*CompositeMultiHandler, LogHandler) {
+       cw := &CompositeMultiHandler{}
+       return cw, cw
+}
+func (h *CompositeMultiHandler) Log(r *Record) (err error) {
+
+       var handler LogHandler
+
+       switch r.Level {
+       case LvlInfo:
+               handler = h.InfoHandler
+       case LvlDebug:
+               handler = h.DebugHandler
+       case LvlWarn:
+               handler = h.WarnHandler
+       case LvlError:
+               handler = h.ErrorHandler
+       case LvlCrit:
+               handler = h.CriticalHandler
+       }
+
+       // Embed the caller function in the context
+       if handler != nil {
+               handler.Log(r)
+       }
+       return
+}
+
+func (h *CompositeMultiHandler) SetHandler(handler LogHandler, replace bool, level LogLevel) {
+       if handler == nil {
+               // Ignore empty handler
+               return
+       }
+       source := &h.DebugHandler
+       switch level {
+       case LvlDebug:
+               source = &h.DebugHandler
+       case LvlInfo:
+               source = &h.InfoHandler
+       case LvlWarn:
+               source = &h.WarnHandler
+       case LvlError:
+               source = &h.ErrorHandler
+       case LvlCrit:
+               source = &h.CriticalHandler
+       }
+
+       if !replace && *source != nil {
+               // If we are not replacing the source make sure that the level handler is applied first
+               if _, isLevel := (*source).(*LevelFilterHandler); !isLevel {
+                       *source = LevelHandler(level, *source)
+               }
+               // If this already was a list add a new logger to it
+               if ll, found := (*source).(*ListLogHandler); found {
+                       ll.Add(handler)
+               } else {
+                       *source = NewListLogHandler(*source, handler)
+               }
+       } else {
+               *source = handler
+       }
+}
+
+// For the multi handler set the handler, using the LogOptions defined
+func (h *CompositeMultiHandler) SetHandlers(handler LogHandler, options *LogOptions) {
+       if len(options.Levels) == 0 {
+               options.Levels = LvlAllList
+       }
+
+       // Set all levels
+       for _, lvl := range options.Levels {
+               h.SetHandler(handler, options.ReplaceExistingHandler, lvl)
+       }
+
+}
+func (h *CompositeMultiHandler) SetJson(writer io.Writer, options *LogOptions) {
+       handler := CallerFileHandler(StreamHandler(writer, JsonFormatEx(
+               options.GetBoolDefault("pretty", false),
+               options.GetBoolDefault("lineSeparated", true),
+       )))
+       if options.HandlerWrap != nil {
+               handler = options.HandlerWrap.SetChild(handler)
+       }
+       h.SetHandlers(handler, options)
+}
+
+// Use built in rolling function
+func (h *CompositeMultiHandler) SetJsonFile(filePath string, options *LogOptions) {
+       writer := &lumberjack.Logger{
+               Filename:   filePath,
+               MaxSize:    options.GetIntDefault("maxSizeMB", 1024), // megabytes
+               MaxAge:     options.GetIntDefault("maxAgeDays", 7),   //days
+               MaxBackups: options.GetIntDefault("maxBackups", 7),
+               Compress:   options.GetBoolDefault("compress", true),
+       }
+       h.SetJson(writer, options)
+}
+
+func (h *CompositeMultiHandler) SetTerminal(writer io.Writer, options *LogOptions) {
+       streamHandler := StreamHandler(
+               writer,
+               TerminalFormatHandler(
+                       options.GetBoolDefault("noColor", false),
+                       options.GetBoolDefault("smallDate", true)))
+
+       if os.Stdout == writer {
+               streamHandler = StreamHandler(
+                       colorable.NewColorableStdout(),
+                       TerminalFormatHandler(
+                               options.GetBoolDefault("noColor", false),
+                               options.GetBoolDefault("smallDate", true)))
+       } else if os.Stderr == writer {
+               streamHandler = StreamHandler(
+                       colorable.NewColorableStderr(),
+                       TerminalFormatHandler(
+                               options.GetBoolDefault("noColor", false),
+                               options.GetBoolDefault("smallDate", true)))
+       }
+       handler := CallerFileHandler(streamHandler)
+
+       if options.HandlerWrap != nil {
+               handler = options.HandlerWrap.SetChild(handler)
+       }
+       h.SetHandlers(handler, options)
+}
+
+// Use built in rolling function
+func (h *CompositeMultiHandler) SetTerminalFile(filePath string, options *LogOptions) {
+       writer := &lumberjack.Logger{
+               Filename:   filePath,
+               MaxSize:    options.GetIntDefault("maxSizeMB", 1024), // megabytes
+               MaxAge:     options.GetIntDefault("maxAgeDays", 7),   //days
+               MaxBackups: options.GetIntDefault("maxBackups", 7),
+               Compress:   options.GetBoolDefault("compress", true),
+       }
+       h.SetTerminal(writer, options)
+}
+
+func (h *CompositeMultiHandler) Disable(levels ...LogLevel) {
+       if len(levels) == 0 {
+               levels = LvlAllList
+       }
+       for _, level := range levels {
+               switch level {
+               case LvlDebug:
+                       h.DebugHandler = nil
+               case LvlInfo:
+                       h.InfoHandler = nil
+               case LvlWarn:
+                       h.WarnHandler = nil
+               case LvlError:
+                       h.ErrorHandler = nil
+               case LvlCrit:
+                       h.CriticalHandler = nil
+               }
+       }
+}
diff --git a/src/foundation/api/revel/logger/doc.go b/src/foundation/api/revel/logger/doc.go
new file mode 100644 (file)
index 0000000..0f6156a
--- /dev/null
@@ -0,0 +1,15 @@
+/*
+  Package logger contains filters and handles for the logging utilities in Revel.
+  These facilities all currently use the logging library called log15 at
+  https://github.com/inconshreveable/log15
+
+       Defining handlers happens as follows
+       1) ALL handlers (log.all.output) replace any existing handlers
+       2) Output handlers (log.error.output) replace any existing handlers
+       3) Filter handlers (log.xxx.filter, log.xxx.nfilter) append to existing handlers,
+          note log.all.filter is treated as a filter handler, so it will NOT replace existing ones
+
+
+
+*/
+package logger
diff --git a/src/foundation/api/revel/logger/handlers.go b/src/foundation/api/revel/logger/handlers.go
new file mode 100644 (file)
index 0000000..dee850e
--- /dev/null
@@ -0,0 +1,210 @@
+package logger
+
+import (
+       "fmt"
+       "io"
+)
+
+type LevelFilterHandler struct {
+       Level LogLevel
+       h     LogHandler
+}
+
+// Filters out records which do not match the level
+// Uses the `log15.FilterHandler` to perform this task
+func LevelHandler(lvl LogLevel, h LogHandler) LogHandler {
+       return &LevelFilterHandler{lvl, h}
+}
+
+// The implementation of the Log
+func (h LevelFilterHandler) Log(r *Record) error {
+       if r.Level == h.Level {
+               return h.h.Log(r)
+       }
+       return nil
+}
+
+// Filters out records which do not match the level
+// Uses the `log15.FilterHandler` to perform this task
+func MinLevelHandler(lvl LogLevel, h LogHandler) LogHandler {
+       return FilterHandler(func(r *Record) (pass bool) {
+               return r.Level <= lvl
+       }, h)
+}
+
+// Filters out records which match the level
+// Uses the `log15.FilterHandler` to perform this task
+func NotLevelHandler(lvl LogLevel, h LogHandler) LogHandler {
+       return FilterHandler(func(r *Record) (pass bool) {
+               return r.Level != lvl
+       }, h)
+}
+
+func CallerFileHandler(h LogHandler) LogHandler {
+       return FuncHandler(func(r *Record) error {
+               r.Context.Add("caller", fmt.Sprint(r.Call))
+               return h.Log(r)
+       })
+}
+
+// Adds in a context called `caller` to the record (contains file name and line number like `foo.go:12`)
+// Uses the `log15.CallerFuncHandler` to perform this task
+func CallerFuncHandler(h LogHandler) LogHandler {
+       return CallerFuncHandler(h)
+}
+
+// Filters out records which match the key value pair
+// Uses the `log15.MatchFilterHandler` to perform this task
+func MatchHandler(key string, value interface{}, h LogHandler) LogHandler {
+       return MatchFilterHandler(key, value, h)
+}
+
+// MatchFilterHandler returns a Handler that only writes records
+// to the wrapped Handler if the given key in the logged
+// context matches the value. For example, to only log records
+// from your ui package:
+//
+//    log.MatchFilterHandler("pkg", "app/ui", log.StdoutHandler)
+//
+func MatchFilterHandler(key string, value interface{}, h LogHandler) LogHandler {
+       return FilterHandler(func(r *Record) (pass bool) {
+               return r.Context[key] == value
+       }, h)
+}
+
+// If match then A handler is called otherwise B handler is called
+func MatchAbHandler(key string, value interface{}, a, b LogHandler) LogHandler {
+       return FuncHandler(func(r *Record) error {
+               if r.Context[key] == value {
+                       return a.Log(r)
+               } else if b != nil {
+                       return b.Log(r)
+               }
+
+               return nil
+       })
+}
+
+// The nil handler is used if logging for a specific request needs to be turned off
+func NilHandler() LogHandler {
+       return FuncHandler(func(r *Record) error {
+               return nil
+       })
+}
+
+// Match all values in map to log
+func MatchMapHandler(matchMap map[string]interface{}, a LogHandler) LogHandler {
+       return matchMapHandler(matchMap, false, a)
+}
+
+// Match !(Match all values in map to log) The inverse of MatchMapHandler
+func NotMatchMapHandler(matchMap map[string]interface{}, a LogHandler) LogHandler {
+       return matchMapHandler(matchMap, true, a)
+}
+
+// Rather then chaining multiple filter handlers, process all here
+func matchMapHandler(matchMap map[string]interface{}, inverse bool, a LogHandler) LogHandler {
+       return FuncHandler(func(r *Record) error {
+               matchCount := 0
+               for k, v := range matchMap {
+                       value, found := r.Context[k]
+                       if !found {
+                               return nil
+                       }
+                       // Test for two failure cases
+                       if value == v && inverse || value != v && !inverse {
+                               return nil
+                       } else {
+                               matchCount++
+                       }
+               }
+               if matchCount != len(matchMap) {
+                       return nil
+               }
+               return a.Log(r)
+       })
+}
+
+// Filters out records which do not match the key value pair
+// Uses the `log15.FilterHandler` to perform this task
+func NotMatchHandler(key string, value interface{}, h LogHandler) LogHandler {
+       return FilterHandler(func(r *Record) (pass bool) {
+               return r.Context[key] != value
+       }, h)
+}
+
+func MultiHandler(hs ...LogHandler) LogHandler {
+       return FuncHandler(func(r *Record) error {
+               for _, h := range hs {
+                       // what to do about failures?
+                       h.Log(r)
+               }
+               return nil
+       })
+}
+
+// StreamHandler writes log records to an io.Writer
+// with the given format. StreamHandler can be used
+// to easily begin writing log records to other
+// outputs.
+//
+// StreamHandler wraps itself with LazyHandler and SyncHandler
+// to evaluate Lazy objects and perform safe concurrent writes.
+func StreamHandler(wr io.Writer, fmtr LogFormat) LogHandler {
+       h := FuncHandler(func(r *Record) error {
+               _, err := wr.Write(fmtr.Format(r))
+               return err
+       })
+       return LazyHandler(SyncHandler(h))
+}
+
+// Filter handler
+func FilterHandler(fn func(r *Record) bool, h LogHandler) LogHandler {
+       return FuncHandler(func(r *Record) error {
+               if fn(r) {
+                       return h.Log(r)
+               }
+               return nil
+       })
+}
+
+// List log handler handles a list of LogHandlers
+type ListLogHandler struct {
+       handlers []LogHandler
+}
+
+// Create a new list of log handlers
+func NewListLogHandler(h1, h2 LogHandler) *ListLogHandler {
+       ll := &ListLogHandler{handlers: []LogHandler{h1, h2}}
+       return ll
+}
+
+// Log the record
+func (ll *ListLogHandler) Log(r *Record) (err error) {
+       for _, handler := range ll.handlers {
+               if err == nil {
+                       err = handler.Log(r)
+               } else {
+                       handler.Log(r)
+               }
+       }
+       return
+}
+
+// Add another log handler
+func (ll *ListLogHandler) Add(h LogHandler) {
+       if h != nil {
+               ll.handlers = append(ll.handlers, h)
+       }
+}
+
+// Remove a log handler
+func (ll *ListLogHandler) Del(h LogHandler) {
+       if h != nil {
+               for i, handler := range ll.handlers {
+                       if handler == h {
+                               ll.handlers = append(ll.handlers[:i], ll.handlers[i+1:]...)
+                       }
+               }
+       }
+}
diff --git a/src/foundation/api/revel/logger/init.go b/src/foundation/api/revel/logger/init.go
new file mode 100644 (file)
index 0000000..6a02cf8
--- /dev/null
@@ -0,0 +1,189 @@
+package logger
+
+// Get all handlers based on the Config (if available)
+import (
+       "fmt"
+       "github.com/revel/config"
+       "log"
+       "os"
+       "path/filepath"
+       "strings"
+)
+
+func InitializeFromConfig(basePath string, config *config.Context) (c *CompositeMultiHandler) {
+       // If running in test mode suppress anything that is not an error
+       if config != nil && config.BoolDefault(TEST_MODE_FLAG, false) {
+               // Preconfigure all the options
+               config.SetOption("log.info.output", "none")
+               config.SetOption("log.debug.output", "none")
+               config.SetOption("log.warn.output", "none")
+               config.SetOption("log.error.output", "stderr")
+               config.SetOption("log.crit.output", "stderr")
+       }
+
+       // If the configuration has an all option we can skip some
+       c, _ = NewCompositeMultiHandler()
+
+       // Filters are assigned first, non filtered items override filters
+       if config != nil && !config.BoolDefault(TEST_MODE_FLAG, false) {
+               initAllLog(c, basePath, config)
+       }
+       initLogLevels(c, basePath, config)
+       if c.CriticalHandler == nil && c.ErrorHandler != nil {
+               c.CriticalHandler = c.ErrorHandler
+       }
+       if config != nil && !config.BoolDefault(TEST_MODE_FLAG, false) {
+               initFilterLog(c, basePath, config)
+               if c.CriticalHandler == nil && c.ErrorHandler != nil {
+                       c.CriticalHandler = c.ErrorHandler
+               }
+               initRequestLog(c, basePath, config)
+       }
+
+       return c
+}
+
+// Init the log.all configuration options
+func initAllLog(c *CompositeMultiHandler, basePath string, config *config.Context) {
+       if config != nil {
+               extraLogFlag := config.BoolDefault(SPECIAL_USE_FLAG, false)
+               if output, found := config.String("log.all.output"); found {
+                       // Set all output for the specified handler
+                       if extraLogFlag {
+                               log.Printf("Adding standard handler for levels to >%s< ", output)
+                       }
+                       initHandlerFor(c, output, basePath, NewLogOptions(config, true, nil, LvlAllList...))
+               }
+       }
+}
+
+// Init the filter options
+// log.all.filter ....
+// log.error.filter ....
+func initFilterLog(c *CompositeMultiHandler, basePath string, config *config.Context) {
+
+       if config != nil {
+               extraLogFlag := config.BoolDefault(SPECIAL_USE_FLAG, false)
+
+               for _, logFilter := range logFilterList {
+                       // Init for all filters
+                       for _, name := range []string{"all", "debug", "info", "warn", "error", "crit",
+                               "trace", // TODO trace is deprecated
+                       } {
+                               optionList := config.Options(logFilter.LogPrefix + name + logFilter.LogSuffix)
+                               for _, option := range optionList {
+                                       splitOptions := strings.Split(option, ".")
+                                       keyMap := map[string]interface{}{}
+                                       for x := 3; x < len(splitOptions); x += 2 {
+                                               keyMap[splitOptions[x]] = splitOptions[x+1]
+                                       }
+                                       phandler := logFilter.parentHandler(keyMap)
+                                       if extraLogFlag {
+                                               log.Printf("Adding key map handler %s %s output %s", option, name, config.StringDefault(option, ""))
+                                               fmt.Printf("Adding key map handler %s %s output %s matching %#v\n", option, name, config.StringDefault(option, ""), keyMap)
+                                       }
+
+                                       if name == "all" {
+                                               initHandlerFor(c, config.StringDefault(option, ""), basePath, NewLogOptions(config, false, phandler))
+                                       } else {
+                                               initHandlerFor(c, config.StringDefault(option, ""), basePath, NewLogOptions(config, false, phandler, toLevel[name]))
+                                       }
+                               }
+                       }
+               }
+       }
+}
+
+// Init the log.error, log.warn etc configuration options
+func initLogLevels(c *CompositeMultiHandler, basePath string, config *config.Context) {
+       for _, name := range []string{"debug", "info", "warn", "error", "crit",
+               "trace", // TODO trace is deprecated
+       } {
+               if config != nil {
+                       extraLogFlag := config.BoolDefault(SPECIAL_USE_FLAG, false)
+                       output, found := config.String("log." + name + ".output")
+                       if found {
+                               if extraLogFlag {
+                                       log.Printf("Adding standard handler %s output %s", name, output)
+                               }
+                               initHandlerFor(c, output, basePath, NewLogOptions(config, true, nil, toLevel[name]))
+                       }
+                       // Gets the list of options with said prefix
+               } else {
+                       initHandlerFor(c, "stderr", basePath, NewLogOptions(config, true, nil, toLevel[name]))
+               }
+       }
+}
+
+// Init the request log options
+func initRequestLog(c *CompositeMultiHandler, basePath string, config *config.Context) {
+       // Request logging to a separate output handler
+       // This takes the InfoHandlers and adds a MatchAbHandler handler to it to direct
+       // context with the word "section=requestlog" to that handler.
+       // Note if request logging is not enabled the MatchAbHandler will not be added and the
+       // request log messages will be sent out the INFO handler
+       outputRequest := "stdout"
+       if config != nil {
+               outputRequest = config.StringDefault("log.request.output", "")
+       }
+       oldInfo := c.InfoHandler
+       c.InfoHandler = nil
+       if outputRequest != "" {
+               initHandlerFor(c, outputRequest, basePath, NewLogOptions(config, false, nil, LvlInfo))
+       }
+       if c.InfoHandler != nil || oldInfo != nil {
+               if c.InfoHandler == nil {
+                       c.InfoHandler = oldInfo
+               } else {
+                       c.InfoHandler = MatchAbHandler("section", "requestlog", c.InfoHandler, oldInfo)
+               }
+       }
+}
+
+// Returns a handler for the level using the output string
+// Accept formats for output string are
+// LogFunctionMap[value] callback function
+// `stdout` `stderr` `full/file/path/to/location/app.log` `full/file/path/to/location/app.json`
+func initHandlerFor(c *CompositeMultiHandler, output, basePath string, options *LogOptions) {
+       if options.Ctx != nil {
+               options.SetExtendedOptions(
+                       "noColor", !options.Ctx.BoolDefault("log.colorize", true),
+                       "smallDate", options.Ctx.BoolDefault("log.smallDate", true),
+                       "maxSize", options.Ctx.IntDefault("log.maxsize", 1024*10),
+                       "maxAge", options.Ctx.IntDefault("log.maxage", 14),
+                       "maxBackups", options.Ctx.IntDefault("log.maxbackups", 14),
+                       "compressBackups", !options.Ctx.BoolDefault("log.compressBackups", true),
+               )
+       }
+
+       output = strings.TrimSpace(output)
+       if funcHandler, found := LogFunctionMap[output]; found {
+               funcHandler(c, options)
+       } else {
+               switch output {
+               case "":
+                       fallthrough
+               case "off":
+               // No handler, discard data
+               default:
+                       // Write to file specified
+                       if !filepath.IsAbs(output) {
+                               output = filepath.Join(basePath, output)
+                       }
+
+                       if err := os.MkdirAll(filepath.Dir(output), 0755); err != nil {
+                               log.Panic(err)
+                       }
+
+                       if strings.HasSuffix(output, "json") {
+                               c.SetJsonFile(output, options)
+                       } else {
+                               // Override defaults for a terminal file
+                               options.SetExtendedOptions("noColor", true)
+                               options.SetExtendedOptions("smallDate", false)
+                               c.SetTerminalFile(output, options)
+                       }
+               }
+       }
+       return
+}
diff --git a/src/foundation/api/revel/logger/init_test.go b/src/foundation/api/revel/logger/init_test.go
new file mode 100644 (file)
index 0000000..50fcd61
--- /dev/null
@@ -0,0 +1,273 @@
+// Copyright (c) 2012-2018 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package logger_test
+
+import (
+       "github.com/revel/config"
+       "github.com/revel/revel/logger"
+       "github.com/stretchr/testify/assert"
+       "os"
+       "strings"
+       "testing"
+)
+
+type (
+       // A counter for the tester
+       testCounter struct {
+               debug, info, warn, error, critical int
+       }
+       // The data to tes
+       testData struct {
+               config []string
+               result testResult
+               tc     *testCounter
+       }
+       // The test result
+       testResult struct {
+               debug, info, warn, error, critical int
+       }
+)
+
+// Single test cases
+var singleCases = []testData{
+       {config: []string{"log.crit.output"},
+               result: testResult{0, 0, 0, 0, 1}},
+       {config: []string{"log.error.output"},
+               result: testResult{0, 0, 0, 1, 1}},
+       {config: []string{"log.warn.output"},
+               result: testResult{0, 0, 1, 0, 0}},
+       {config: []string{"log.info.output"},
+               result: testResult{0, 1, 0, 0, 0}},
+       {config: []string{"log.debug.output"},
+               result: testResult{1, 0, 0, 0, 0}},
+}
+
+// Test singles
+func TestSingleCases(t *testing.T) {
+       rootLog := logger.New()
+       for _, testCase := range singleCases {
+               testCase.logTest(rootLog, t)
+               testCase.validate(t)
+       }
+}
+
+// Filter test cases
+var filterCases = []testData{
+       {config: []string{"log.crit.filter.module.app"},
+               result: testResult{0, 0, 0, 0, 1}},
+       {config: []string{"log.crit.filter.module.appa"},
+               result: testResult{0, 0, 0, 0, 0}},
+       {config: []string{"log.error.filter.module.app"},
+               result: testResult{0, 0, 0, 1, 1}},
+       {config: []string{"log.error.filter.module.appa"},
+               result: testResult{0, 0, 0, 0, 0}},
+       {config: []string{"log.warn.filter.module.app"},
+               result: testResult{0, 0, 1, 0, 0}},
+       {config: []string{"log.warn.filter.module.appa"},
+               result: testResult{0, 0, 0, 0, 0}},
+       {config: []string{"log.info.filter.module.app"},
+               result: testResult{0, 1, 0, 0, 0}},
+       {config: []string{"log.info.filter.module.appa"},
+               result: testResult{0, 0, 0, 0, 0}},
+       {config: []string{"log.debug.filter.module.app"},
+               result: testResult{1, 0, 0, 0, 0}},
+       {config: []string{"log.debug.filter.module.appa"},
+               result: testResult{0, 0, 0, 0, 0}},
+}
+
+// Filter test
+func TestFilterCases(t *testing.T) {
+       rootLog := logger.New("module", "app")
+       for _, testCase := range filterCases {
+               testCase.logTest(rootLog, t)
+               testCase.validate(t)
+       }
+}
+
+// Inverse test cases
+var nfilterCases = []testData{
+       {config: []string{"log.crit.nfilter.module.appa"},
+               result: testResult{0, 0, 0, 0, 1}},
+       {config: []string{"log.crit.nfilter.modules.appa"},
+               result: testResult{0, 0, 0, 0, 0}},
+       {config: []string{"log.crit.nfilter.module.app"},
+               result: testResult{0, 0, 0, 0, 0}},
+       {config: []string{"log.error.nfilter.module.appa"}, // Special case, when error is not nill critical inherits from error
+               result: testResult{0, 0, 0, 1, 1}},
+       {config: []string{"log.error.nfilter.module.app"},
+               result: testResult{0, 0, 0, 0, 0}},
+       {config: []string{"log.warn.nfilter.module.appa"},
+               result: testResult{0, 0, 1, 0, 0}},
+       {config: []string{"log.warn.nfilter.module.app"},
+               result: testResult{0, 0, 0, 0, 0}},
+       {config: []string{"log.info.nfilter.module.appa"},
+               result: testResult{0, 1, 0, 0, 0}},
+       {config: []string{"log.info.nfilter.module.app"},
+               result: testResult{0, 0, 0, 0, 0}},
+       {config: []string{"log.debug.nfilter.module.appa"},
+               result: testResult{1, 0, 0, 0, 0}},
+       {config: []string{"log.debug.nfilter.module.app"},
+               result: testResult{0, 0, 0, 0, 0}},
+}
+
+// Inverse test
+func TestNotFilterCases(t *testing.T) {
+       rootLog := logger.New("module", "app")
+       for _, testCase := range nfilterCases {
+               testCase.logTest(rootLog, t)
+               testCase.validate(t)
+       }
+}
+
+// off test cases
+var offCases = []testData{
+       {config: []string{"log.all.output", "log.error.output=off"},
+               result: testResult{1, 1, 1, 0, 1}},
+}
+
+// Off test
+func TestOffCases(t *testing.T) {
+       rootLog := logger.New("module", "app")
+       for _, testCase := range offCases {
+               testCase.logTest(rootLog, t)
+               testCase.validate(t)
+       }
+}
+
+// Duplicate test cases
+var duplicateCases = []testData{
+       {config: []string{"log.all.output", "log.error.output", "log.error.filter.module.app"},
+               result: testResult{1, 1, 1, 2, 1}},
+}
+
+// test duplicate cases
+func TestDuplicateCases(t *testing.T) {
+       rootLog := logger.New("module", "app")
+       for _, testCase := range duplicateCases {
+               testCase.logTest(rootLog, t)
+               testCase.validate(t)
+       }
+}
+
+// Contradicting cases
+var contradictCases = []testData{
+       {config: []string{"log.all.output", "log.error.output=off", "log.all.output"},
+               result: testResult{1, 1, 1, 0, 1}},
+       {config: []string{"log.all.output", "log.error.output=off", "log.debug.filter.module.app"},
+               result: testResult{2, 1, 1, 0, 1}},
+       {config: []string{"log.all.filter.module.app", "log.info.output=off", "log.info.filter.module.app"},
+               result: testResult{1, 2, 1, 1, 1}},
+       {config: []string{"log.all.output", "log.info.output=off", "log.info.filter.module.app"},
+               result: testResult{1, 1, 1, 1, 1}},
+}
+
+// Contradiction test
+func TestContradictCases(t *testing.T) {
+       rootLog := logger.New("module", "app")
+       for _, testCase := range contradictCases {
+               testCase.logTest(rootLog, t)
+               testCase.validate(t)
+       }
+}
+
+// All test cases
+var allCases = []testData{
+       {config: []string{"log.all.filter.module.app"},
+               result: testResult{1, 1, 1, 1, 1}},
+       {config: []string{"log.all.output"},
+               result: testResult{2, 2, 2, 2, 2}},
+}
+
+// All tests
+func TestAllCases(t *testing.T) {
+       rootLog := logger.New("module", "app")
+       for i, testCase := range allCases {
+               testCase.logTest(rootLog, t)
+               allCases[i] = testCase
+       }
+       rootLog = logger.New()
+       for i, testCase := range allCases {
+               testCase.logTest(rootLog, t)
+               allCases[i] = testCase
+       }
+       for _, testCase := range allCases {
+               testCase.validate(t)
+       }
+
+}
+
+func (c *testCounter) Log(r *logger.Record) error {
+       switch r.Level {
+       case logger.LvlDebug:
+               c.debug++
+       case logger.LvlInfo:
+               c.info++
+       case logger.LvlWarn:
+               c.warn++
+       case logger.LvlError:
+               c.error++
+       case logger.LvlCrit:
+               c.critical++
+       default:
+               panic("Unknown log level")
+       }
+       return nil
+}
+func (td *testData) logTest(rootLog logger.MultiLogger, t *testing.T) {
+       if td.tc == nil {
+               td.tc = &testCounter{}
+               counterInit(td.tc)
+       }
+       newContext := config.NewContext()
+       for _, i := range td.config {
+               iout := strings.Split(i, "=")
+               if len(iout) > 1 {
+                       newContext.SetOption(iout[0], iout[1])
+               } else {
+                       newContext.SetOption(i, "test")
+               }
+       }
+
+       newContext.SetOption("specialUseFlag", "true")
+
+       handler := logger.InitializeFromConfig("test", newContext)
+
+       rootLog.SetHandler(handler)
+
+       td.runLogTest(rootLog)
+}
+
+func (td *testData) runLogTest(log logger.MultiLogger) {
+       log.Debug("test")
+       log.Info("test")
+       log.Warn("test")
+       log.Error("test")
+       log.Crit("test")
+}
+
+func (td *testData) validate(t *testing.T) {
+       t.Logf("Test %#v expected %#v", td.tc, td.result)
+       assert.Equal(t, td.result.debug, td.tc.debug, "Debug failed "+strings.Join(td.config, " "))
+       assert.Equal(t, td.result.info, td.tc.info, "Info failed "+strings.Join(td.config, " "))
+       assert.Equal(t, td.result.warn, td.tc.warn, "Warn failed "+strings.Join(td.config, " "))
+       assert.Equal(t, td.result.error, td.tc.error, "Error failed "+strings.Join(td.config, " "))
+       assert.Equal(t, td.result.critical, td.tc.critical, "Critical failed "+strings.Join(td.config, " "))
+}
+
+// Add test to the function map
+func counterInit(tc *testCounter) {
+       logger.LogFunctionMap["test"] = func(c *logger.CompositeMultiHandler, logOptions *logger.LogOptions) {
+               // Output to the test log and the stdout
+               outHandler := logger.LogHandler(
+                       logger.NewListLogHandler(tc,
+                               logger.StreamHandler(os.Stdout, logger.TerminalFormatHandler(false, true))),
+               )
+               if logOptions.HandlerWrap != nil {
+                       outHandler = logOptions.HandlerWrap.SetChild(outHandler)
+               }
+
+               c.SetHandlers(outHandler, logOptions)
+       }
+}
diff --git a/src/foundation/api/revel/logger/log_function_map.go b/src/foundation/api/revel/logger/log_function_map.go
new file mode 100644 (file)
index 0000000..ebbc76d
--- /dev/null
@@ -0,0 +1,37 @@
+package logger
+
+import (
+       "os"
+)
+
+// The log function map can be added to, so that you can specify your own logging mechanism
+// it has defaults for off, stdout, stderr
+var LogFunctionMap = map[string]func(*CompositeMultiHandler, *LogOptions){
+       // Do nothing - set the logger off
+       "off": func(c *CompositeMultiHandler, logOptions *LogOptions) {
+               // Only drop the results if there is a parent handler defined
+               if logOptions.HandlerWrap != nil {
+                       for _, l := range logOptions.Levels {
+                               c.SetHandler(logOptions.HandlerWrap.SetChild(NilHandler()), logOptions.ReplaceExistingHandler, l)
+                       }
+               } else {
+                       // Clear existing handler
+                       c.SetHandlers(NilHandler(), logOptions)
+               }
+       },
+       // Do nothing - set the logger off
+       "": func(*CompositeMultiHandler, *LogOptions) {},
+       // Set the levels to stdout, replace existing
+       "stdout": func(c *CompositeMultiHandler, logOptions *LogOptions) {
+               if logOptions.Ctx != nil {
+                       logOptions.SetExtendedOptions(
+                               "noColor", !logOptions.Ctx.BoolDefault("log.colorize", true),
+                               "smallDate", logOptions.Ctx.BoolDefault("log.smallDate", true))
+               }
+               c.SetTerminal(os.Stdout, logOptions)
+       },
+       // Set the levels to stderr output to terminal
+       "stderr": func(c *CompositeMultiHandler, logOptions *LogOptions) {
+               c.SetTerminal(os.Stderr, logOptions)
+       },
+}
diff --git a/src/foundation/api/revel/logger/logger.go b/src/foundation/api/revel/logger/logger.go
new file mode 100644 (file)
index 0000000..b9abdaf
--- /dev/null
@@ -0,0 +1,203 @@
+package logger
+
+import (
+       "fmt"
+       "github.com/revel/config"
+       "time"
+)
+
+// The LogHandler defines the interface to handle the log records
+type (
+       // The Multilogger reduces the number of exposed defined logging variables,
+       // and allows the output to be easily refined
+       MultiLogger interface {
+               // New returns a new Logger that has this logger's context plus the given context
+               New(ctx ...interface{}) MultiLogger
+
+               // SetHandler updates the logger to write records to the specified handler.
+               SetHandler(h LogHandler)
+
+               // Set the stack depth for the logger
+               SetStackDepth(int) MultiLogger
+
+               // Log a message at the given level with context key/value pairs
+               Debug(msg string, ctx ...interface{})
+
+               // Log a message at the given level formatting message with the parameters
+               Debugf(msg string, params ...interface{})
+
+               // Log a message at the given level with context key/value pairs
+               Info(msg string, ctx ...interface{})
+
+               // Log a message at the given level formatting message with the parameters
+               Infof(msg string, params ...interface{})
+
+               // Log a message at the given level with context key/value pairs
+               Warn(msg string, ctx ...interface{})
+
+               // Log a message at the given level formatting message with the parameters
+               Warnf(msg string, params ...interface{})
+
+               // Log a message at the given level with context key/value pairs
+               Error(msg string, ctx ...interface{})
+
+               // Log a message at the given level formatting message with the parameters
+               Errorf(msg string, params ...interface{})
+
+               // Log a message at the given level with context key/value pairs
+               Crit(msg string, ctx ...interface{})
+
+               // Log a message at the given level formatting message with the parameters
+               Critf(msg string, params ...interface{})
+
+               // Log a message at the given level with context key/value pairs and exits
+               Fatal(msg string, ctx ...interface{})
+
+               // Log a message at the given level formatting message with the parameters and exits
+               Fatalf(msg string, params ...interface{})
+
+               // Log a message at the given level with context key/value pairs and panics
+               Panic(msg string, ctx ...interface{})
+
+               // Log a message at the given level formatting message with the parameters and panics
+               Panicf(msg string, params ...interface{})
+       }
+
+       // The log handler interface
+       LogHandler interface {
+               Log(*Record) error
+               //log15.Handler
+       }
+
+       // The log stack handler interface
+       LogStackHandler interface {
+               LogHandler
+               GetStack() int
+       }
+
+       // The log handler interface which has child logs
+       ParentLogHandler interface {
+               SetChild(handler LogHandler) LogHandler
+       }
+
+       // The log format interface
+       LogFormat interface {
+               Format(r *Record) []byte
+       }
+
+       // The log level type
+       LogLevel int
+
+       // Used for the callback to LogFunctionMap
+       LogOptions struct {
+               Ctx                    *config.Context
+               ReplaceExistingHandler bool
+               HandlerWrap            ParentLogHandler
+               Levels                 []LogLevel
+               ExtendedOptions        map[string]interface{}
+       }
+
+       // The log record
+       Record struct {
+               Message string     // The message
+               Time    time.Time  // The time
+               Level   LogLevel   //The level
+               Call    CallStack  // The call stack if built
+               Context ContextMap // The context
+       }
+
+       // The lazy structure to implement a function to be invoked only if needed
+       Lazy struct {
+               Fn interface{} // the function
+       }
+
+       // Currently the only requirement for the callstack is to support the Formatter method
+       // which stack.Call does so we use that
+       CallStack interface {
+               fmt.Formatter // Requirement
+       }
+)
+
+// FormatFunc returns a new Format object which uses
+// the given function to perform record formatting.
+func FormatFunc(f func(*Record) []byte) LogFormat {
+       return formatFunc(f)
+}
+
+type formatFunc func(*Record) []byte
+
+func (f formatFunc) Format(r *Record) []byte {
+       return f(r)
+}
+func NewRecord(message string, level LogLevel) *Record {
+       return &Record{Message: message, Context: ContextMap{}, Level: level}
+}
+
+const (
+       LvlCrit  LogLevel = iota // Critical
+       LvlError                 // Error
+       LvlWarn                  // Warning
+       LvlInfo                  // Information
+       LvlDebug                 // Debug
+)
+
+// A list of all the log levels
+var LvlAllList = []LogLevel{LvlDebug, LvlInfo, LvlWarn, LvlError, LvlCrit}
+
+// Implements the ParentLogHandler
+type parentLogHandler struct {
+       setChild func(handler LogHandler) LogHandler
+}
+
+// Create a new parent log handler
+func NewParentLogHandler(callBack func(child LogHandler) LogHandler) ParentLogHandler {
+       return &parentLogHandler{callBack}
+}
+
+// Sets the child of the log handler
+func (p *parentLogHandler) SetChild(child LogHandler) LogHandler {
+       return p.setChild(child)
+}
+
+// Create a new log options
+func NewLogOptions(cfg *config.Context, replaceHandler bool, phandler ParentLogHandler, lvl ...LogLevel) (logOptions *LogOptions) {
+       logOptions = &LogOptions{
+               Ctx: cfg,
+               ReplaceExistingHandler: replaceHandler,
+               HandlerWrap:            phandler,
+               Levels:                 lvl,
+               ExtendedOptions:        map[string]interface{}{},
+       }
+       return
+}
+
+// Assumes options will be an even number and have a string, value syntax
+func (l *LogOptions) SetExtendedOptions(options ...interface{}) {
+       for x := 0; x < len(options); x += 2 {
+               l.ExtendedOptions[options[x].(string)] = options[x+1]
+       }
+}
+
+// Gets a string option with default
+func (l *LogOptions) GetStringDefault(option, value string) string {
+       if v, found := l.ExtendedOptions[option]; found {
+               return v.(string)
+       }
+       return value
+}
+
+// Gets an int option with default
+func (l *LogOptions) GetIntDefault(option string, value int) int {
+       if v, found := l.ExtendedOptions[option]; found {
+               return v.(int)
+       }
+       return value
+}
+
+// Gets a boolean option with default
+func (l *LogOptions) GetBoolDefault(option string, value bool) bool {
+       if v, found := l.ExtendedOptions[option]; found {
+               return v.(bool)
+       }
+       return value
+}
diff --git a/src/foundation/api/revel/logger/revel_logger.go b/src/foundation/api/revel/logger/revel_logger.go
new file mode 100644 (file)
index 0000000..b3a6f86
--- /dev/null
@@ -0,0 +1,142 @@
+package logger
+
+import (
+       "fmt"
+       "github.com/revel/log15"
+       "log"
+       "os"
+)
+
+// This type implements the MultiLogger
+type RevelLogger struct {
+       log15.Logger
+}
+
+// Set the systems default logger
+// Default logs will be captured and handled by revel at level info
+func SetDefaultLog(fromLog MultiLogger) {
+       log.SetOutput(loggerRewrite{Logger: fromLog, Level: log15.LvlInfo, hideDeprecated: true})
+       // No need to show date and time, that will be logged with revel
+       log.SetFlags(0)
+}
+
+func (rl *RevelLogger) Debugf(msg string, param ...interface{}) {
+       rl.Debug(fmt.Sprintf(msg, param...))
+}
+
+// Print a formatted info message
+func (rl *RevelLogger) Infof(msg string, param ...interface{}) {
+       rl.Info(fmt.Sprintf(msg, param...))
+}
+
+// Print a formatted warn message
+func (rl *RevelLogger) Warnf(msg string, param ...interface{}) {
+       rl.Warn(fmt.Sprintf(msg, param...))
+}
+
+// Print a formatted error message
+func (rl *RevelLogger) Errorf(msg string, param ...interface{}) {
+       rl.Error(fmt.Sprintf(msg, param...))
+}
+
+// Print a formatted critical message
+func (rl *RevelLogger) Critf(msg string, param ...interface{}) {
+       rl.Crit(fmt.Sprintf(msg, param...))
+}
+
+// Print a formatted fatal message
+func (rl *RevelLogger) Fatalf(msg string, param ...interface{}) {
+       rl.Fatal(fmt.Sprintf(msg, param...))
+}
+
+// Print a formatted panic message
+func (rl *RevelLogger) Panicf(msg string, param ...interface{}) {
+       rl.Panic(fmt.Sprintf(msg, param...))
+}
+
+// Print a critical message and call os.Exit(1)
+func (rl *RevelLogger) Fatal(msg string, ctx ...interface{}) {
+       rl.Crit(msg, ctx...)
+       os.Exit(1)
+}
+
+// Print a critical message and panic
+func (rl *RevelLogger) Panic(msg string, ctx ...interface{}) {
+       rl.Crit(msg, ctx...)
+       panic(msg)
+}
+
+// Override log15 method
+func (rl *RevelLogger) New(ctx ...interface{}) MultiLogger {
+       old := &RevelLogger{Logger: rl.Logger.New(ctx...)}
+       return old
+}
+
+// Set the stack level to check for the caller
+func (rl *RevelLogger) SetStackDepth(amount int) MultiLogger {
+       rl.Logger.SetStackDepth(amount) // Ignore the logger returned
+       return rl
+}
+
+// Create a new logger
+func New(ctx ...interface{}) MultiLogger {
+       r := &RevelLogger{Logger: log15.New(ctx...)}
+       r.SetStackDepth(1)
+       return r
+}
+
+// Set the handler in the Logger
+func (rl *RevelLogger) SetHandler(h LogHandler) {
+       rl.Logger.SetHandler(callHandler(h.Log))
+}
+
+// The function wrapper to implement the callback
+type callHandler func(r *Record) error
+
+// Log implementation, reads the record and extracts the details from the log record
+// Hiding the implementation.
+func (c callHandler) Log(log *log15.Record) error {
+       ctx := log.Ctx
+       var ctxMap ContextMap
+       if len(ctx) > 0 {
+               ctxMap = make(ContextMap, len(ctx)/2)
+
+               for i := 0; i < len(ctx); i += 2 {
+                       v := ctx[i]
+                       key, ok := v.(string)
+                       if !ok {
+                               key = fmt.Sprintf("LOGGER_INVALID_KEY %v", v)
+                       }
+                       var value interface{}
+                       if len(ctx) > i+1 {
+                               value = ctx[i+1]
+                       } else {
+                               value = "LOGGER_VALUE_MISSING"
+                       }
+                       ctxMap[key] = value
+               }
+       } else {
+               ctxMap = make(ContextMap, 0)
+       }
+       r := &Record{Message: log.Msg, Context: ctxMap, Time: log.Time, Level: LogLevel(log.Lvl), Call: CallStack(log.Call)}
+       return c(r)
+}
+
+// Internally used contextMap, allows conversion of map to map[string]string
+type ContextMap map[string]interface{}
+
+// Convert the context map to be string only values, any non string values are ignored
+func (m ContextMap) StringMap() (newMap map[string]string) {
+       if m != nil {
+               newMap = map[string]string{}
+               for key, value := range m {
+                       if svalue, isstring := value.(string); isstring {
+                               newMap[key] = svalue
+                       }
+               }
+       }
+       return
+}
+func (m ContextMap) Add(key string, value interface{}) {
+       m[key] = value
+}
diff --git a/src/foundation/api/revel/logger/terminal_format.go b/src/foundation/api/revel/logger/terminal_format.go
new file mode 100644 (file)
index 0000000..ca2cd15
--- /dev/null
@@ -0,0 +1,245 @@
+package logger
+
+import (
+       "bytes"
+       "encoding/json"
+       "fmt"
+       "reflect"
+       "strconv"
+       "sync"
+       "time"
+)
+
+const (
+       timeFormat          = "2006-01-02T15:04:05-0700"
+       termTimeFormat      = "2006/01/02 15:04:05"
+       termSmallTimeFormat = "15:04:05"
+       floatFormat         = 'f'
+       errorKey            = "REVEL_ERROR"
+)
+
+var (
+       levelString = map[LogLevel]string{LvlDebug: "DEBUG",
+               LvlInfo: "INFO", LvlWarn: "WARN", LvlError: "ERROR", LvlCrit: "CRIT"}
+)
+
+// Outputs to the terminal in a format like below
+// INFO  09:11:32 server-engine.go:169: Request Stats
+func TerminalFormatHandler(noColor bool, smallDate bool) LogFormat {
+       dateFormat := termTimeFormat
+       if smallDate {
+               dateFormat = termSmallTimeFormat
+       }
+       return FormatFunc(func(r *Record) []byte {
+               // Bash coloring http://misc.flogisoft.com/bash/tip_colors_and_formatting
+               var color = 0
+               switch r.Level {
+               case LvlCrit:
+                       // Magenta
+                       color = 35
+               case LvlError:
+                       // Red
+                       color = 31
+               case LvlWarn:
+                       // Yellow
+                       color = 33
+               case LvlInfo:
+                       // Green
+                       color = 32
+               case LvlDebug:
+                       // Cyan
+                       color = 36
+               }
+
+               b := &bytes.Buffer{}
+               caller, _ := r.Context["caller"].(string)
+               module, _ := r.Context["module"].(string)
+               if noColor == false && color > 0 {
+                       if len(module) > 0 {
+                               fmt.Fprintf(b, "\x1b[%dm%-5s\x1b[0m %s %6s %13s: %-40s ", color, levelString[r.Level], r.Time.Format(dateFormat), module, caller, r.Message)
+                       } else {
+                               fmt.Fprintf(b, "\x1b[%dm%-5s\x1b[0m %s %13s: %-40s ", color, levelString[r.Level], r.Time.Format(dateFormat), caller, r.Message)
+                       }
+               } else {
+                       fmt.Fprintf(b, "%-5s %s %6s %13s: %-40s", levelString[r.Level], r.Time.Format(dateFormat), module, caller, r.Message)
+               }
+
+               i := 0
+               for k, v := range r.Context {
+                       if i != 0 {
+                               b.WriteByte(' ')
+                       }
+                       i++
+                       if k == "module" || k == "caller" {
+                               continue
+                       }
+
+                       v := formatLogfmtValue(v)
+
+                       // TODO: we should probably check that all of your key bytes aren't invalid
+                       if noColor == false && color > 0 {
+                               fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m=%s", color, k, v)
+                       } else {
+                               b.WriteString(k)
+                               b.WriteByte('=')
+                               b.WriteString(v)
+                       }
+               }
+
+               b.WriteByte('\n')
+
+               return b.Bytes()
+       })
+}
+
+// formatValue formats a value for serialization
+func formatLogfmtValue(value interface{}) string {
+       if value == nil {
+               return "nil"
+       }
+
+       if t, ok := value.(time.Time); ok {
+               // Performance optimization: No need for escaping since the provided
+               // timeFormat doesn't have any escape characters, and escaping is
+               // expensive.
+               return t.Format(termTimeFormat)
+       }
+       value = formatShared(value)
+       switch v := value.(type) {
+       case bool:
+               return strconv.FormatBool(v)
+       case float32:
+               return strconv.FormatFloat(float64(v), floatFormat, 3, 64)
+       case float64:
+               return strconv.FormatFloat(v, floatFormat, 7, 64)
+       case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
+               return fmt.Sprintf("%d", value)
+       case string:
+               return escapeString(v)
+       default:
+               return escapeString(fmt.Sprintf("%+v", value))
+       }
+}
+
+// Format the value in json format
+func formatShared(value interface{}) (result interface{}) {
+       defer func() {
+               if err := recover(); err != nil {
+                       if v := reflect.ValueOf(value); v.Kind() == reflect.Ptr && v.IsNil() {
+                               result = "nil"
+                       } else {
+                               panic(err)
+                       }
+               }
+       }()
+
+       switch v := value.(type) {
+       case time.Time:
+               return v.Format(timeFormat)
+
+       case error:
+               return v.Error()
+
+       case fmt.Stringer:
+               return v.String()
+
+       default:
+               return v
+       }
+}
+
+// A reusuable buffer for outputting data
+var stringBufPool = sync.Pool{
+       New: func() interface{} { return new(bytes.Buffer) },
+}
+
+// Escape the string when needed
+func escapeString(s string) string {
+       needsQuotes := false
+       needsEscape := false
+       for _, r := range s {
+               if r <= ' ' || r == '=' || r == '"' {
+                       needsQuotes = true
+               }
+               if r == '\\' || r == '"' || r == '\n' || r == '\r' || r == '\t' {
+                       needsEscape = true
+               }
+       }
+       if needsEscape == false && needsQuotes == false {
+               return s
+       }
+       e := stringBufPool.Get().(*bytes.Buffer)
+       e.WriteByte('"')
+       for _, r := range s {
+               switch r {
+               case '\\', '"':
+                       e.WriteByte('\\')
+                       e.WriteByte(byte(r))
+               case '\n':
+                       e.WriteString("\\n")
+               case '\r':
+                       e.WriteString("\\r")
+               case '\t':
+                       e.WriteString("\\t")
+               default:
+                       e.WriteRune(r)
+               }
+       }
+       e.WriteByte('"')
+       var ret string
+       if needsQuotes {
+               ret = e.String()
+       } else {
+               ret = string(e.Bytes()[1 : e.Len()-1])
+       }
+       e.Reset()
+       stringBufPool.Put(e)
+       return ret
+}
+
+// JsonFormatEx formats log records as JSON objects. If pretty is true,
+// records will be pretty-printed. If lineSeparated is true, records
+// will be logged with a new line between each record.
+func JsonFormatEx(pretty, lineSeparated bool) LogFormat {
+       jsonMarshal := json.Marshal
+       if pretty {
+               jsonMarshal = func(v interface{}) ([]byte, error) {
+                       return json.MarshalIndent(v, "", "    ")
+               }
+       }
+
+       return FormatFunc(func(r *Record) []byte {
+               props := make(map[string]interface{})
+
+               props["t"] = r.Time
+               props["lvl"] = levelString[r.Level]
+               props["msg"] = r.Message
+               for k, v := range r.Context {
+                       props[k] = formatJsonValue(v)
+               }
+
+               b, err := jsonMarshal(props)
+               if err != nil {
+                       b, _ = jsonMarshal(map[string]string{
+                               errorKey: err.Error(),
+                       })
+                       return b
+               }
+
+               if lineSeparated {
+                       b = append(b, '\n')
+               }
+
+               return b
+       })
+}
+
+func formatJsonValue(value interface{}) interface{} {
+       value = formatShared(value)
+       switch value.(type) {
+       case int, int8, int16, int32, int64, float32, float64, uint, uint8, uint16, uint32, uint64, string:
+               return value
+       default:
+               return fmt.Sprintf("%+v", value)
+       }
+}
diff --git a/src/foundation/api/revel/logger/utils.go b/src/foundation/api/revel/logger/utils.go
new file mode 100644 (file)
index 0000000..3918cc1
--- /dev/null
@@ -0,0 +1,110 @@
+package logger
+
+import (
+       "github.com/revel/log15"
+       "gopkg.in/stack.v0"
+       "log"
+)
+
+// Utility package to make existing logging backwards compatible
+var (
+       // Convert the string to LogLevel
+       toLevel = map[string]LogLevel{"debug": LogLevel(log15.LvlDebug),
+               "info": LogLevel(log15.LvlInfo), "request": LogLevel(log15.LvlInfo), "warn": LogLevel(log15.LvlWarn),
+               "error": LogLevel(log15.LvlError), "crit": LogLevel(log15.LvlCrit),
+               "trace": LogLevel(log15.LvlDebug), // TODO trace is deprecated, replaced by debug
+       }
+)
+
+const (
+       // The test mode flag overrides the default log level and shows only errors
+       TEST_MODE_FLAG = "testModeFlag"
+       // The special use flag enables showing messages when the logger is setup
+       SPECIAL_USE_FLAG = "specialUseFlag"
+)
+
+// Returns the logger for the name
+func GetLogger(name string, logger MultiLogger) (l *log.Logger) {
+       switch name {
+       case "trace": // TODO trace is deprecated, replaced by debug
+               l = log.New(loggerRewrite{Logger: logger, Level: log15.LvlDebug}, "", 0)
+       case "debug":
+               l = log.New(loggerRewrite{Logger: logger, Level: log15.LvlDebug}, "", 0)
+       case "info":
+               l = log.New(loggerRewrite{Logger: logger, Level: log15.LvlInfo}, "", 0)
+       case "warn":
+               l = log.New(loggerRewrite{Logger: logger, Level: log15.LvlWarn}, "", 0)
+       case "error":
+               l = log.New(loggerRewrite{Logger: logger, Level: log15.LvlError}, "", 0)
+       case "request":
+               l = log.New(loggerRewrite{Logger: logger, Level: log15.LvlInfo}, "", 0)
+       }
+
+       return l
+
+}
+
+// Used by the initFilterLog to handle the filters
+var logFilterList = []struct {
+       LogPrefix, LogSuffix string
+       parentHandler        func(map[string]interface{}) ParentLogHandler
+}{{
+       "log.", ".filter",
+       func(keyMap map[string]interface{}) ParentLogHandler {
+               return NewParentLogHandler(func(child LogHandler) LogHandler {
+                       return MatchMapHandler(keyMap, child)
+               })
+
+       },
+}, {
+       "log.", ".nfilter",
+       func(keyMap map[string]interface{}) ParentLogHandler {
+               return NewParentLogHandler(func(child LogHandler) LogHandler {
+                       return NotMatchMapHandler(keyMap, child)
+               })
+       },
+}}
+
+// This structure and method will handle the old output format and log it to the new format
+type loggerRewrite struct {
+       Logger         MultiLogger
+       Level          log15.Lvl
+       hideDeprecated bool
+}
+
+// The message indicating that a logger is using a deprecated log mechanism
+var log_deprecated = []byte("* LOG DEPRECATED * ")
+
+// Implements the Write of the logger
+func (lr loggerRewrite) Write(p []byte) (n int, err error) {
+       if !lr.hideDeprecated {
+               p = append(log_deprecated, p...)
+       }
+       n = len(p)
+       if len(p) > 0 && p[n-1] == '\n' {
+               p = p[:n-1]
+               n--
+       }
+
+       switch lr.Level {
+       case log15.LvlInfo:
+               lr.Logger.Info(string(p))
+       case log15.LvlDebug:
+               lr.Logger.Debug(string(p))
+       case log15.LvlWarn:
+               lr.Logger.Warn(string(p))
+       case log15.LvlError:
+               lr.Logger.Error(string(p))
+       case log15.LvlCrit:
+               lr.Logger.Crit(string(p))
+       }
+
+       return
+}
+
+// For logging purposes the call stack can be used to record the stack trace of a bad error
+// simply pass it as a context field in your log statement like
+// `controller.Log.Crit("This should not occur","stack",revel.NewCallStack())`
+func NewCallStack() interface{} {
+       return stack.Trace()
+}
diff --git a/src/foundation/api/revel/logger/wrap_handlers.go b/src/foundation/api/revel/logger/wrap_handlers.go
new file mode 100644 (file)
index 0000000..3d68e75
--- /dev/null
@@ -0,0 +1,98 @@
+package logger
+
+// FuncHandler returns a Handler that logs records with the given
+// function.
+import (
+       "fmt"
+       "reflect"
+       "sync"
+       "time"
+)
+
+// Function handler wraps the declared function and returns the handler for it
+func FuncHandler(fn func(r *Record) error) LogHandler {
+       return funcHandler(fn)
+}
+
+// The type decleration for the function
+type funcHandler func(r *Record) error
+
+// The implementation of the Log
+func (h funcHandler) Log(r *Record) error {
+       return h(r)
+}
+
+// This function allows you to do a full declaration for the log,
+// it is recommended you use FuncHandler instead
+func HandlerFunc(log func(message string, time time.Time, level LogLevel, call CallStack, context ContextMap) error) LogHandler {
+       return remoteHandler(log)
+}
+
+// The type used for the HandlerFunc
+type remoteHandler func(message string, time time.Time, level LogLevel, call CallStack, context ContextMap) error
+
+// The Log implementation
+func (c remoteHandler) Log(record *Record) error {
+       return c(record.Message, record.Time, record.Level, record.Call, record.Context)
+}
+
+// SyncHandler can be wrapped around a handler to guarantee that
+// only a single Log operation can proceed at a time. It's necessary
+// for thread-safe concurrent writes.
+func SyncHandler(h LogHandler) LogHandler {
+       var mu sync.Mutex
+       return FuncHandler(func(r *Record) error {
+               defer mu.Unlock()
+               mu.Lock()
+               return h.Log(r)
+       })
+}
+
+// LazyHandler writes all values to the wrapped handler after evaluating
+// any lazy functions in the record's context. It is already wrapped
+// around StreamHandler and SyslogHandler in this library, you'll only need
+// it if you write your own Handler.
+func LazyHandler(h LogHandler) LogHandler {
+       return FuncHandler(func(r *Record) error {
+               for k, v := range r.Context {
+                       if lz, ok := v.(Lazy); ok {
+                               value, err := evaluateLazy(lz)
+                               if err != nil {
+                                       r.Context[errorKey] = "bad lazy " + k
+                               } else {
+                                       v = value
+                               }
+                       }
+               }
+
+               return h.Log(r)
+       })
+}
+
+func evaluateLazy(lz Lazy) (interface{}, error) {
+       t := reflect.TypeOf(lz.Fn)
+
+       if t.Kind() != reflect.Func {
+               return nil, fmt.Errorf("INVALID_LAZY, not func: %+v", lz.Fn)
+       }
+
+       if t.NumIn() > 0 {
+               return nil, fmt.Errorf("INVALID_LAZY, func takes args: %+v", lz.Fn)
+       }
+
+       if t.NumOut() == 0 {
+               return nil, fmt.Errorf("INVALID_LAZY, no func return val: %+v", lz.Fn)
+       }
+
+       value := reflect.ValueOf(lz.Fn)
+       results := value.Call([]reflect.Value{})
+       if len(results) == 1 {
+               return results[0].Interface(), nil
+       } else {
+               values := make([]interface{}, len(results))
+               for i, v := range results {
+                       values[i] = v.Interface()
+               }
+               return values, nil
+       }
+}
diff --git a/src/foundation/api/revel/model/revel_container.go b/src/foundation/api/revel/model/revel_container.go
new file mode 100644 (file)
index 0000000..c3398d9
--- /dev/null
@@ -0,0 +1,14 @@
+package model
+
+import "github.com/revel/revel/utils"
+
+// The single instance object that has the config populated to it
+type RevelContainer struct {
+       Controller struct {
+               Reuse              bool                              // True if the controllers are reused Set via revel.controller.reuse
+               Stack              *utils.SimpleLockStack            // size set by revel.controller.stack,  revel.controller.maxstack
+               CachedMap          map[string]*utils.SimpleLockStack // The map of reusable controllers
+               CachedStackSize    int                               // The default size of each stack in CachedMap Set via revel.cache.controller.stack
+               CachedStackMaxSize int                               // The max size of each stack in CachedMap Set via revel.cache.controller.maxstack
+       }
+}
diff --git a/src/foundation/api/revel/module.go b/src/foundation/api/revel/module.go
new file mode 100644 (file)
index 0000000..e51ac76
--- /dev/null
@@ -0,0 +1,213 @@
+package revel
+
+import (
+       "fmt"
+       "github.com/revel/revel/logger"
+       "go/build"
+       "gopkg.in/stack.v0"
+       "path/filepath"
+       "sort"
+       "strings"
+)
+
+// Module specific functions
+type Module struct {
+       Name, ImportPath, Path string
+       ControllerTypeList     []*ControllerType
+       Log                    logger.MultiLogger
+       initializedModules     map[string]ModuleCallbackInterface
+}
+
+// Modules can be called back after they are loaded in revel by using this interface.
+type ModuleCallbackInterface func(*Module)
+
+// The namespace separator constant
+const namespaceSeperator = `\` // (note cannot be . or : as this is already used for routes)
+
+var (
+       Modules   []*Module                                                                                     // The list of modules in use
+       anyModule = &Module{}                                                                                   // Wildcard search for controllers for a module (for backward compatible lookups)
+       appModule = &Module{Name: "App", initializedModules: map[string]ModuleCallbackInterface{}, Log: AppLog} // The app module
+       moduleLog = RevelLog.New("section", "module")
+)
+
+// Called by a module init() function, caller will receive the *Module object created for that module
+// This would be useful for assigning a logger for logging information in the module (since the module context would be correct)
+func RegisterModuleInit(callback ModuleCallbackInterface) {
+       // Store the module that called this so we can do a callback when the app is initialized
+       // The format %+k is from go-stack/Call.Format and returns the package path
+       key := fmt.Sprintf("%+k", stack.Caller(1))
+       appModule.initializedModules[key] = callback
+       if Initialized {
+               RevelLog.Error("Application already initialized, initializing using app module", "key", key)
+               callback(appModule)
+       }
+}
+
+// Called on startup to make a callback so that modules can be initialized through the `RegisterModuleInit` function
+func init() {
+       AddInitEventHandler(func(typeOf Event, value interface{}) (responseOf EventResponse) {
+               if typeOf == REVEL_BEFORE_MODULES_LOADED {
+                       Modules = []*Module{appModule}
+                       appModule.Path = filepath.ToSlash(AppPath)
+                       appModule.ImportPath = filepath.ToSlash(AppPath)
+               }
+
+               return
+       })
+}
+
+// Returns the namespace for the module in the format `module_name|`
+func (m *Module) Namespace() (namespace string) {
+       namespace = m.Name + namespaceSeperator
+       return
+}
+
+// Returns the named controller and action that is in this module
+func (m *Module) ControllerByName(name, action string) (ctype *ControllerType) {
+       comparison := name
+       if strings.Index(name, namespaceSeperator) < 0 {
+               comparison = m.Namespace() + name
+       }
+       for _, c := range m.ControllerTypeList {
+               if c.Name() == comparison {
+                       ctype = c
+                       break
+               }
+       }
+       return
+}
+
+// Adds the controller type to this module
+func (m *Module) AddController(ct *ControllerType) {
+       m.ControllerTypeList = append(m.ControllerTypeList, ct)
+}
+
+// Based on the full path given return the relevant module
+// Only to be used on initialization
+func ModuleFromPath(path string, addGopathToPath bool) (module *Module) {
+       path = filepath.ToSlash(path)
+       gopathList := filepath.SplitList(build.Default.GOPATH)
+       // Strip away the vendor folder
+       if i := strings.Index(path, "/vendor/"); i > 0 {
+               path = path[i+len("vendor/"):]
+       }
+
+       // See if the path exists in the module based
+       for i := range Modules {
+               if addGopathToPath {
+                       for _, gopath := range gopathList {
+                               if strings.Contains(filepath.ToSlash(filepath.Clean(filepath.Join(gopath, "src", path))), Modules[i].Path) {
+                                       module = Modules[i]
+                                       break
+                               }
+                       }
+               } else {
+                       if strings.Contains(path, Modules[i].ImportPath) {
+                               module = Modules[i]
+                               break
+                       }
+
+               }
+
+               if module != nil {
+                       break
+               }
+       }
+       // Default to the app module if not found
+       if module == nil {
+               module = appModule
+       }
+       return
+}
+
+// ModuleByName returns the module of the given name, if loaded, case insensitive.
+func ModuleByName(name string) (*Module, bool) {
+       // If the name ends with the namespace separator remove it
+       if name[len(name)-1] == []byte(namespaceSeperator)[0] {
+               name = name[:len(name)-1]
+       }
+       name = strings.ToLower(name)
+       if name == strings.ToLower(appModule.Name) {
+               return appModule, true
+       }
+       for _, module := range Modules {
+               if strings.ToLower(module.Name) == name {
+                       return module, true
+               }
+       }
+       return nil, false
+}
+
+// Loads the modules specified in the config
+func loadModules() {
+       keys := []string{}
+       for _, key := range Config.Options("module.") {
+               keys = append(keys, key)
+       }
+
+       // Reorder module order by key name, a poor mans sort but at least it is consistent
+       sort.Strings(keys)
+       for _, key := range keys {
+               moduleLog.Debug("Sorted keys", "keys", key)
+
+       }
+       for _, key := range keys {
+               moduleImportPath := Config.StringDefault(key, "")
+               if moduleImportPath == "" {
+                       continue
+               }
+
+               modulePath, err := ResolveImportPath(moduleImportPath)
+               if err != nil {
+                       moduleLog.Error("Failed to load module.  Import of path failed", "modulePath", moduleImportPath, "error", err)
+               }
+               // Drop anything between module.???.<name of module>
+               subKey := key[len("module."):]
+               if index := strings.Index(subKey, "."); index > -1 {
+                       subKey = subKey[index+1:]
+               }
+               addModule(subKey, moduleImportPath, modulePath)
+       }
+
+       // Modules loaded, now show module path
+       for key, callback := range appModule.initializedModules {
+               if m := ModuleFromPath(key, false); m != nil {
+                       callback(m)
+               } else {
+                       RevelLog.Error("Callback for non registered module initializing with application module", "modulePath", key)
+                       callback(appModule)
+               }
+       }
+}
+
+// called by `loadModules`, creates a new `Module` instance and appends it to the `Modules` list
+func addModule(name, importPath, modulePath string) {
+       if _, found := ModuleByName(name); found {
+               moduleLog.Panic("Attempt to import duplicate module %s path %s aborting startup", "name", name, "path", modulePath)
+       }
+       Modules = append(Modules, &Module{Name: name,
+               ImportPath: filepath.ToSlash(importPath),
+               Path:       filepath.ToSlash(modulePath),
+               Log:        RootLog.New("module", name)})
+       if codePath := filepath.Join(modulePath, "app"); DirExists(codePath) {
+               CodePaths = append(CodePaths, codePath)
+               if viewsPath := filepath.Join(modulePath, "app", "views"); DirExists(viewsPath) {
+                       TemplatePaths = append(TemplatePaths, viewsPath)
+               }
+       }
+
+       moduleLog.Debug("Loaded module ", "module", filepath.Base(modulePath))
+
+       // Hack: There is presently no way for the testrunner module to add the
+       // "test" subdirectory to the CodePaths.  So this does it instead.
+       if importPath == Config.StringDefault("module.testrunner", "github.com/revel/modules/testrunner") {
+               joinedPath := filepath.Join(BasePath, "tests")
+               moduleLog.Debug("Found testrunner module, adding `tests` path ", "path", joinedPath)
+               CodePaths = append(CodePaths, joinedPath)
+       }
+       if testsPath := filepath.Join(modulePath, "tests"); DirExists(testsPath) {
+               moduleLog.Debug("Found tests path ", "path", testsPath)
+               CodePaths = append(CodePaths, testsPath)
+       }
+}
diff --git a/src/foundation/api/revel/namespace.go b/src/foundation/api/revel/namespace.go
new file mode 100644 (file)
index 0000000..9f66cf4
--- /dev/null
@@ -0,0 +1,37 @@
+package revel
+
+import (
+       "bytes"
+       "regexp"
+)
+
+// Module matching template syntax allows for modules to replace this text with the name of the module declared on import
+// this allows the reverse router to use correct syntax
+// Match _LOCAL_.static or  _LOCAL_|
+var namespaceReplacement = regexp.MustCompile(`(_LOCAL_)(\.(.*?))?\\`)
+
+// Function to replace the bytes data that may match the _LOCAL_ namespace specifier,
+// the replacement will be the current module.Name
+func namespaceReplace(fileBytes []byte, module *Module) []byte {
+       newBytes, lastIndex := &bytes.Buffer{}, 0
+       matches := namespaceReplacement.FindAllSubmatchIndex(fileBytes, -1)
+       for _, match := range matches {
+               // Write up to first bytes
+               newBytes.Write(fileBytes[lastIndex:match[0]])
+               // skip ahead index to match[1]
+               lastIndex = match[3]
+               if match[4] > 0 {
+                       // This match includes the module name as imported by the module
+                       // We could transform the module name if it is different..
+                       // For now leave it the same
+                       // so _LOCAL_.static| becomes static|
+                       lastIndex++
+               } else {
+                       // Inject the module name
+                       newBytes.Write([]byte(module.Name))
+               }
+       }
+       // Write remainder of document
+       newBytes.Write(fileBytes[lastIndex:])
+       return newBytes.Bytes()
+}
diff --git a/src/foundation/api/revel/panic.go b/src/foundation/api/revel/panic.go
new file mode 100644 (file)
index 0000000..464f2de
--- /dev/null
@@ -0,0 +1,50 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "fmt"
+       "net/http"
+       "runtime/debug"
+)
+
+// PanicFilter wraps the action invocation in a protective defer blanket that
+// converts panics into 500 error pages.
+func PanicFilter(c *Controller, fc []Filter) {
+       defer func() {
+               if err := recover(); err != nil {
+                       handleInvocationPanic(c, err)
+               }
+       }()
+       fc[0](c, fc[1:])
+}
+
+// This function handles a panic in an action invocation.
+// It cleans up the stack trace, logs it, and displays an error page.
+func handleInvocationPanic(c *Controller, err interface{}) {
+       error := NewErrorFromPanic(err)
+       if error != nil {
+               utilLog.Error("PanicFilter: Caught panic", "error", err, "stack", error.Stack)
+               if DevMode {
+                       fmt.Println(err)
+                       fmt.Println(error.Stack)
+               }
+       } else {
+               utilLog.Error("PanicFilter: Caught panic, unable to determine stack location", "error", err, "stack", string(debug.Stack()))
+               if DevMode {
+                       fmt.Println(err)
+                       fmt.Println("stack", string(debug.Stack()))
+               }
+       }
+
+       if error == nil && DevMode {
+               // Only show the sensitive information in the debug stack trace in development mode, not production
+               c.Response.SetStatus(http.StatusInternalServerError)
+               _, _ = c.Response.GetWriter().Write(debug.Stack())
+               return
+       }
+
+       c.Result = c.RenderError(error)
+}
diff --git a/src/foundation/api/revel/params.go b/src/foundation/api/revel/params.go
new file mode 100644 (file)
index 0000000..3df3dde
--- /dev/null
@@ -0,0 +1,180 @@
+// Copyright (c) 2012-2017 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "encoding/json"
+       "errors"
+       "io/ioutil"
+       "mime/multipart"
+       "net/url"
+       "os"
+       "reflect"
+)
+
+// Params provides a unified view of the request params.
+// Includes:
+// - URL query string
+// - Form values
+// - File uploads
+//
+// Warning: param maps other than Values may be nil if there were none.
+type Params struct {
+       url.Values // A unified view of all the individual param maps below.
+
+       // Set by the router
+       Fixed url.Values // Fixed parameters from the route, e.g. App.Action("fixed param")
+       Route url.Values // Parameters extracted from the route,  e.g. /customers/{id}
+
+       // Set by the ParamsFilter
+       Query url.Values // Parameters from the query string, e.g. /index?limit=10
+       Form  url.Values // Parameters from the request body.
+
+       Files    map[string][]*multipart.FileHeader // Files uploaded in a multipart form
+       tmpFiles []*os.File                         // Temp files used during the request.
+       JSON     []byte                             // JSON data from request body
+}
+
+var paramsLogger = RevelLog.New("section", "params")
+
+// ParseParams parses the `http.Request` params into `revel.Controller.Params`
+func ParseParams(params *Params, req *Request) {
+       params.Query = req.GetQuery()
+
+       // Parse the body depending on the content type.
+       switch req.ContentType {
+       case "application/x-www-form-urlencoded":
+               // Typical form.
+               var err error
+               if params.Form, err = req.GetForm(); err != nil {
+                       paramsLogger.Warn("ParseParams: Error parsing request body", "error", err)
+               }
+
+       case "multipart/form-data":
+               // Multipart form.
+               if mp, err := req.GetMultipartForm(); err != nil {
+                       paramsLogger.Warn("ParseParams: parsing request body:", "error", err)
+               } else {
+                       params.Form = mp.GetValues()
+                       params.Files = mp.GetFiles()
+               }
+       case "application/json":
+               fallthrough
+       case "text/json":
+               if body := req.GetBody(); body != nil {
+                       if content, err := ioutil.ReadAll(body); err == nil {
+                               // We wont bind it until we determine what we are binding too
+                               params.JSON = content
+                       } else {
+                               paramsLogger.Error("ParseParams: Failed to ready request body bytes", "error", err)
+                       }
+               } else {
+                       paramsLogger.Info("ParseParams: Json post received with empty body")
+               }
+       }
+
+       params.Values = params.calcValues()
+}
+
+// Bind looks for the named parameter, converts it to the requested type, and
+// writes it into "dest", which must be settable.  If the value can not be
+// parsed, "dest" is set to the zero value.
+func (p *Params) Bind(dest interface{}, name string) {
+       value := reflect.ValueOf(dest)
+       if value.Kind() != reflect.Ptr {
+               paramsLogger.Panic("Bind: revel/params: non-pointer passed to Bind: " + name)
+       }
+       value = value.Elem()
+       if !value.CanSet() {
+               paramsLogger.Panic("Bind: revel/params: non-settable variable passed to Bind: " + name)
+       }
+
+       // Remove the json from the Params, this will stop the binder from attempting
+       // to use the json data to populate the destination interface. We do not want
+       // to do this on a named bind directly against the param, it is ok to happen when
+       // the action is invoked.
+       jsonData := p.JSON
+       p.JSON = nil
+       value.Set(Bind(p, name, value.Type()))
+       p.JSON = jsonData
+}
+
+// Bind binds the JSON data to the dest.
+func (p *Params) BindJSON(dest interface{}) error {
+       value := reflect.ValueOf(dest)
+       if value.Kind() != reflect.Ptr {
+               paramsLogger.Warn("BindJSON: Not a pointer")
+               return errors.New("BindJSON not a pointer")
+       }
+       if err := json.Unmarshal(p.JSON, dest); err != nil {
+               paramsLogger.Warn("BindJSON: Unable to unmarshal request:", "error", err)
+               return err
+       }
+       return nil
+}
+
+// calcValues returns a unified view of the component param maps.
+func (p *Params) calcValues() url.Values {
+       numParams := len(p.Query) + len(p.Fixed) + len(p.Route) + len(p.Form)
+
+       // If there were no params, return an empty map.
+       if numParams == 0 {
+               return make(url.Values, 0)
+       }
+
+       // If only one of the param sources has anything, return that directly.
+       switch numParams {
+       case len(p.Query):
+               return p.Query
+       case len(p.Route):
+               return p.Route
+       case len(p.Fixed):
+               return p.Fixed
+       case len(p.Form):
+               return p.Form
+       }
+
+       // Copy everything into a param map,
+       // order of priority is least to most trusted
+       values := make(url.Values, numParams)
+
+       // ?query string parameters are first
+       for k, v := range p.Query {
+               values[k] = append(values[k], v...)
+       }
+
+       // form parameters append
+       for k, v := range p.Form {
+               values[k] = append(values[k], v...)
+       }
+
+       // :/path parameters overwrite
+       for k, v := range p.Route {
+               values[k] = v
+       }
+
+       // fixed route parameters overwrite
+       for k, v := range p.Fixed {
+               values[k] = v
+       }
+
+       return values
+}
+
+func ParamsFilter(c *Controller, fc []Filter) {
+       ParseParams(c.Params, c.Request)
+
+       // Clean up from the request.
+       defer func() {
+               for _, tmpFile := range c.Params.tmpFiles {
+                       err := os.Remove(tmpFile.Name())
+                       if err != nil {
+                               paramsLogger.Warn("ParamsFilter: Could not remove upload temp file:", err)
+                       }
+               }
+       }()
+
+       fc[0](c, fc[1:])
+}
diff --git a/src/foundation/api/revel/params_test.go b/src/foundation/api/revel/params_test.go
new file mode 100644 (file)
index 0000000..571a244
--- /dev/null
@@ -0,0 +1,182 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "bytes"
+       "fmt"
+       "io/ioutil"
+       "net/http"
+       "net/url"
+       "reflect"
+       "testing"
+)
+
+// Params: Testing Multipart forms
+
+const (
+       MultipartBoundary = "A"
+       MultipartFormData = `--A
+Content-Disposition: form-data; name="text1"
+
+data1
+--A
+Content-Disposition: form-data; name="text2"
+
+data2
+--A
+Content-Disposition: form-data; name="text2"
+
+data3
+--A
+Content-Disposition: form-data; name="file1"; filename="test.txt"
+Content-Type: text/plain
+
+content1
+--A
+Content-Disposition: form-data; name="file2[]"; filename="test.txt"
+Content-Type: text/plain
+
+content2
+--A
+Content-Disposition: form-data; name="file2[]"; filename="favicon.ico"
+Content-Type: image/x-icon
+
+xyz
+--A
+Content-Disposition: form-data; name="file3[0]"; filename="test.txt"
+Content-Type: text/plain
+
+content3
+--A
+Content-Disposition: form-data; name="file3[1]"; filename="favicon.ico"
+Content-Type: image/x-icon
+
+zzz
+--A--
+`
+)
+
+// The values represented by the form data.
+type fh struct {
+       filename string
+       content  []byte
+}
+
+var (
+       expectedValues = map[string][]string{
+               "text1": {"data1"},
+               "text2": {"data2", "data3"},
+       }
+       expectedFiles = map[string][]fh{
+               "file1":    {fh{"test.txt", []byte("content1")}},
+               "file2[]":  {fh{"test.txt", []byte("content2")}, fh{"favicon.ico", []byte("xyz")}},
+               "file3[0]": {fh{"test.txt", []byte("content3")}},
+               "file3[1]": {fh{"favicon.ico", []byte("zzz")}},
+       }
+)
+
+func getMultipartRequest() *http.Request {
+       req, _ := http.NewRequest("POST", "http://localhost/path",
+               bytes.NewBufferString(MultipartFormData))
+       req.Header.Set(
+               "Content-Type", fmt.Sprintf("multipart/form-data; boundary=%s", MultipartBoundary))
+       req.Header.Set(
+               "Content-Length", fmt.Sprintf("%d", len(MultipartFormData)))
+       return req
+}
+
+func BenchmarkParams(b *testing.B) {
+       c := NewTestController(nil, showRequest)
+       c.Params = &Params{}
+
+       for i := 0; i < b.N; i++ {
+               ParamsFilter(c, NilChain)
+       }
+}
+
+func TestMultipartForm(t *testing.T) {
+       c := NewTestController(nil, getMultipartRequest())
+       c.Params = &Params{}
+
+       ParamsFilter(c, NilChain)
+
+       if !reflect.DeepEqual(expectedValues, map[string][]string(c.Params.Values)) {
+               t.Errorf("Param values: (expected) %v != %v (actual)",
+                       expectedValues, map[string][]string(c.Params.Values))
+       }
+
+       actualFiles := make(map[string][]fh)
+       for key, fileHeaders := range c.Params.Files {
+               for _, fileHeader := range fileHeaders {
+                       file, _ := fileHeader.Open()
+                       content, _ := ioutil.ReadAll(file)
+                       actualFiles[key] = append(actualFiles[key], fh{fileHeader.Filename, content})
+               }
+       }
+
+       if !reflect.DeepEqual(expectedFiles, actualFiles) {
+               t.Errorf("Param files: (expected) %v != %v (actual)", expectedFiles, actualFiles)
+       }
+}
+
+func TestBind(t *testing.T) {
+       params := Params{
+               Values: url.Values{
+                       "x": {"5"},
+               },
+       }
+       var x int
+       params.Bind(&x, "x")
+       if x != 5 {
+               t.Errorf("Failed to bind x.  Value: %d", x)
+       }
+}
+
+func TestResolveAcceptLanguage(t *testing.T) {
+       request := buildHTTPRequestWithAcceptLanguage("")
+       if result := ResolveAcceptLanguage(request); result != nil {
+               t.Errorf("Expected Accept-Language to resolve to an empty string but it was '%s'", result)
+       }
+
+       request = buildHTTPRequestWithAcceptLanguage("en-GB,en;q=0.8,nl;q=0.6")
+       if result := ResolveAcceptLanguage(request); len(result) != 3 {
+               t.Errorf("Unexpected Accept-Language values length of %d (expected %d)", len(result), 3)
+       } else {
+               if result[0].Language != "en-GB" {
+                       t.Errorf("Expected '%s' to be most qualified but instead it's '%s'", "en-GB", result[0].Language)
+               }
+               if result[1].Language != "en" {
+                       t.Errorf("Expected '%s' to be most qualified but instead it's '%s'", "en", result[1].Language)
+               }
+               if result[2].Language != "nl" {
+                       t.Errorf("Expected '%s' to be most qualified but instead it's '%s'", "nl", result[2].Language)
+               }
+       }
+
+       request = buildHTTPRequestWithAcceptLanguage("en;q=0.8,nl;q=0.6,en-AU;q=malformed")
+       if result := ResolveAcceptLanguage(request); len(result) != 3 {
+               t.Errorf("Unexpected Accept-Language values length of %d (expected %d)", len(result), 3)
+       } else {
+               if result[0].Language != "en-AU" {
+                       t.Errorf("Expected '%s' to be most qualified but instead it's '%s'", "en-AU", result[0].Language)
+               }
+       }
+}
+
+func BenchmarkResolveAcceptLanguage(b *testing.B) {
+       for i := 0; i < b.N; i++ {
+               request := buildHTTPRequestWithAcceptLanguage("en-GB,en;q=0.8,nl;q=0.6,fr;q=0.5,de-DE;q=0.4,no-NO;q=0.4,ru;q=0.2")
+               ResolveAcceptLanguage(request)
+       }
+}
+
+func buildHTTPRequestWithAcceptLanguage(acceptLanguage string) *Request {
+       request, _ := http.NewRequest("POST", "http://localhost/path", nil)
+       request.Header.Set("Accept-Language", acceptLanguage)
+       c := NewTestController(nil, request)
+
+       return c.Request
+}
diff --git a/src/foundation/api/revel/results.go b/src/foundation/api/revel/results.go
new file mode 100644 (file)
index 0000000..59d0507
--- /dev/null
@@ -0,0 +1,513 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "bytes"
+       "encoding/json"
+       "encoding/xml"
+       "errors"
+       "fmt"
+       "io"
+       "io/ioutil"
+       "net/http"
+       "reflect"
+       "strconv"
+       "strings"
+       "time"
+)
+
+type Result interface {
+       Apply(req *Request, resp *Response)
+}
+
+// ErrorResult structure used to handles all kinds of error codes (500, 404, ..).
+// It renders the relevant error page (errors/CODE.format, e.g. errors/500.json).
+// If RunMode is "dev", this results in a friendly error page.
+type ErrorResult struct {
+       ViewArgs map[string]interface{}
+       Error    error
+}
+
+var resultsLog = RevelLog.New("section", "results")
+
+func (r ErrorResult) Apply(req *Request, resp *Response) {
+       format := req.Format
+       status := resp.Status
+       if status == 0 {
+               status = http.StatusInternalServerError
+       }
+
+       contentType := ContentTypeByFilename("xxx." + format)
+       if contentType == DefaultFileContentType {
+               contentType = "text/plain"
+       }
+       lang, _ := r.ViewArgs[CurrentLocaleViewArg].(string)
+       // Get the error template.
+       var err error
+       templatePath := fmt.Sprintf("errors/%d.%s", status, format)
+       tmpl, err := MainTemplateLoader.TemplateLang(templatePath, lang)
+
+       // This func shows a plaintext error message, in case the template rendering
+       // doesn't work.
+       showPlaintext := func(err error) {
+               PlaintextErrorResult{fmt.Errorf("Server Error:\n%s\n\n"+
+                       "Additionally, an error occurred when rendering the error page:\n%s",
+                       r.Error, err)}.Apply(req, resp)
+       }
+
+       if tmpl == nil {
+               if err == nil {
+                       err = fmt.Errorf("Couldn't find template %s", templatePath)
+               }
+               templateLog.Warn("Got an error rendering template", "error", err, "template", templatePath, "lang", lang)
+               showPlaintext(err)
+               return
+       }
+
+       // If it's not a revel error, wrap it in one.
+       var revelError *Error
+       switch e := r.Error.(type) {
+       case *Error:
+               revelError = e
+       case error:
+               revelError = &Error{
+                       Title:       "Server Error",
+                       Description: e.Error(),
+               }
+       }
+
+       if revelError == nil {
+               panic("no error provided")
+       }
+
+       if r.ViewArgs == nil {
+               r.ViewArgs = make(map[string]interface{})
+       }
+       r.ViewArgs["RunMode"] = RunMode
+       r.ViewArgs["DevMode"] = DevMode
+       r.ViewArgs["Error"] = revelError
+       r.ViewArgs["Router"] = MainRouter
+
+       resultsLog.Info("Rendering error template", "template", templatePath, "error", revelError)
+
+       // Render it.
+       var b bytes.Buffer
+       err = tmpl.Render(&b, r.ViewArgs)
+
+       // If there was an error, print it in plain text.
+       if err != nil {
+               templateLog.Warn("Got an error rendering template", "error", err, "template", templatePath, "lang", lang)
+               showPlaintext(err)
+               return
+       }
+
+       // need to check if we are on a websocket here
+       // net/http panics if we write to a hijacked connection
+       if req.Method == "WS" {
+               if err := req.WebSocket.MessageSendJSON(fmt.Sprint(revelError)); err != nil {
+                       resultsLog.Error("Apply: Send failed", "error", err)
+               }
+       } else {
+               resp.WriteHeader(status, contentType)
+               if _, err := b.WriteTo(resp.GetWriter()); err != nil {
+                       resultsLog.Error("Apply: Response WriteTo failed:", "error", err)
+               }
+       }
+
+}
+
+type PlaintextErrorResult struct {
+       Error error
+}
+
+// Apply method is used when the template loader or error template is not available.
+func (r PlaintextErrorResult) Apply(req *Request, resp *Response) {
+       resp.WriteHeader(http.StatusInternalServerError, "text/plain; charset=utf-8")
+       if _, err := resp.GetWriter().Write([]byte(r.Error.Error())); err != nil {
+               resultsLog.Error("Apply: Write error:", "error", err)
+       }
+}
+
+// RenderTemplateResult action methods returns this result to request
+// a template be rendered.
+type RenderTemplateResult struct {
+       Template Template
+       ViewArgs map[string]interface{}
+}
+
+func (r *RenderTemplateResult) Apply(req *Request, resp *Response) {
+       // Handle panics when rendering templates.
+       defer func() {
+               if err := recover(); err != nil {
+                       resultsLog.Error("Apply: panic recovery", "error", err)
+                       PlaintextErrorResult{fmt.Errorf("Template Execution Panic in %s:\n%s",
+                               r.Template.Name(), err)}.Apply(req, resp)
+               }
+       }()
+
+       chunked := Config.BoolDefault("results.chunked", false)
+
+       // If it's a HEAD request, throw away the bytes.
+       out := io.Writer(resp.GetWriter())
+       if req.Method == "HEAD" {
+               out = ioutil.Discard
+       }
+
+       // In a prod mode, write the status, render, and hope for the best.
+       // (In a dev mode, always render to a temporary buffer first to avoid having
+       // error pages distorted by HTML already written)
+       if chunked && !DevMode {
+               resp.WriteHeader(http.StatusOK, "text/html; charset=utf-8")
+               if err := r.renderOutput(out); err != nil {
+                       r.renderError(err, req, resp)
+               }
+               return
+       }
+
+       // Render the template into a temporary buffer, to see if there was an error
+       // rendering the template.  If not, then copy it into the response buffer.
+       // Otherwise, template render errors may result in unpredictable HTML (and
+       // would carry a 200 status code)
+       b, err := r.ToBytes()
+       if err != nil {
+               r.renderError(err, req, resp)
+               return
+       }
+
+       if !chunked {
+               resp.Out.Header().Set("Content-Length", strconv.Itoa(b.Len()))
+       }
+       resp.WriteHeader(http.StatusOK, "text/html; charset=utf-8")
+       if _, err := b.WriteTo(out); err != nil {
+               resultsLog.Error("Apply: Response write failed", "error", err)
+       }
+}
+
+// Return a byte array and or an error object if the template failed to render
+func (r *RenderTemplateResult) ToBytes() (b *bytes.Buffer, err error) {
+       defer func() {
+               if rerr := recover(); rerr != nil {
+                       resultsLog.Error("ApplyBytes: panic recovery", "recover-error", rerr)
+                       err = fmt.Errorf("Template Execution Panic in %s:\n%s", r.Template.Name(), rerr)
+               }
+       }()
+       b = &bytes.Buffer{}
+       if err = r.renderOutput(b); err == nil {
+               if Config.BoolDefault("results.trim.html", false) {
+                       b = r.compressHtml(b)
+               }
+       }
+       return
+}
+
+// Output the template to the writer, catch any panics and return as an error
+func (r *RenderTemplateResult) renderOutput(wr io.Writer) (err error) {
+       defer func() {
+               if rerr := recover(); rerr != nil {
+                       resultsLog.Error("ApplyBytes: panic recovery", "recover-error", rerr)
+                       err = fmt.Errorf("Template Execution Panic in %s:\n%s", r.Template.Name(), rerr)
+               }
+       }()
+       err = r.Template.Render(wr, r.ViewArgs)
+       return
+}
+
+// Trimming the HTML will do the following:
+// * Remove all leading & trailing whitespace on every line
+// * Remove all empty lines
+// * Attempt to keep formatting inside <pre></pre> tags
+//
+// This is safe unless white-space: pre; is used in css for formatting.
+// Since there is no way to detect that, you will have to keep trimming off in these cases.
+func (r *RenderTemplateResult) compressHtml(b *bytes.Buffer) (b2 *bytes.Buffer) {
+
+       // Allocate length of original buffer, so we can write everything without allocating again
+       b2.Grow(b.Len())
+       insidePre := false
+       for {
+               text, err := b.ReadString('\n')
+               // Convert to lower case for finding <pre> tags.
+               tl := strings.ToLower(text)
+               if strings.Contains(tl, "<pre>") {
+                       insidePre = true
+               }
+               // Trim if not inside a <pre> statement
+               if !insidePre {
+                       // Cut trailing/leading whitespace
+                       text = strings.Trim(text, " \t\r\n")
+                       if len(text) > 0 {
+                               if _, err = b2.WriteString(text); err != nil {
+                                       resultsLog.Error("Apply: ", "error", err)
+                               }
+                               if _, err = b2.WriteString("\n"); err != nil {
+                                       resultsLog.Error("Apply: ", "error", err)
+                               }
+                       }
+               } else {
+                       if _, err = b2.WriteString(text); err != nil {
+                               resultsLog.Error("Apply: ", "error", err)
+                       }
+               }
+               if strings.Contains(tl, "</pre>") {
+                       insidePre = false
+               }
+               // We are finished
+               if err != nil {
+                       break
+               }
+       }
+
+       return
+}
+
+// Render the error in the response
+func (r *RenderTemplateResult) renderError(err error, req *Request, resp *Response) {
+       compileError, found := err.(*Error)
+       if !found {
+               var templateContent []string
+               templateName, line, description := ParseTemplateError(err)
+               if templateName == "" {
+                       templateLog.Info("Cannot determine template name to render error", "error", err)
+                       templateName = r.Template.Name()
+                       templateContent = r.Template.Content()
+
+               } else {
+                       lang, _ := r.ViewArgs[CurrentLocaleViewArg].(string)
+                       if tmpl, err := MainTemplateLoader.TemplateLang(templateName, lang); err == nil {
+                               templateContent = tmpl.Content()
+                       } else {
+                               templateLog.Info("Unable to retreive template ", "error", err)
+                       }
+               }
+               compileError = &Error{
+                       Title:       "Template Execution Error",
+                       Path:        templateName,
+                       Description: description,
+                       Line:        line,
+                       SourceLines: templateContent,
+               }
+       }
+       resp.Status = 500
+       resultsLog.Errorf("render: Template Execution Error (in %s): %s", compileError.Path, compileError.Description)
+       ErrorResult{r.ViewArgs, compileError}.Apply(req, resp)
+}
+
+type RenderHTMLResult struct {
+       html string
+}
+
+func (r RenderHTMLResult) Apply(req *Request, resp *Response) {
+       resp.WriteHeader(http.StatusOK, "text/html; charset=utf-8")
+       if _, err := resp.GetWriter().Write([]byte(r.html)); err != nil {
+               resultsLog.Error("Apply: Response write failed", "error", err)
+       }
+}
+
+type RenderJSONResult struct {
+       obj      interface{}
+       callback string
+}
+
+func (r RenderJSONResult) Apply(req *Request, resp *Response) {
+       var b []byte
+       var err error
+       if Config.BoolDefault("results.pretty", false) {
+               b, err = json.MarshalIndent(r.obj, "", "  ")
+       } else {
+               b, err = json.Marshal(r.obj)
+       }
+
+       if err != nil {
+               ErrorResult{Error: err}.Apply(req, resp)
+               return
+       }
+
+       if r.callback == "" {
+               resp.WriteHeader(http.StatusOK, "application/json; charset=utf-8")
+               if _, err = resp.GetWriter().Write(b); err != nil {
+                       resultsLog.Error("Apply: Response write failed:", "error", err)
+               }
+               return
+       }
+
+       resp.WriteHeader(http.StatusOK, "application/javascript; charset=utf-8")
+       if _, err = resp.GetWriter().Write([]byte(r.callback + "(")); err != nil {
+               resultsLog.Error("Apply: Response write failed", "error", err)
+       }
+       if _, err = resp.GetWriter().Write(b); err != nil {
+               resultsLog.Error("Apply: Response write failed", "error", err)
+       }
+       if _, err = resp.GetWriter().Write([]byte(");")); err != nil {
+               resultsLog.Error("Apply: Response write failed", "error", err)
+       }
+}
+
+type RenderXMLResult struct {
+       obj interface{}
+}
+
+func (r RenderXMLResult) Apply(req *Request, resp *Response) {
+       var b []byte
+       var err error
+       if Config.BoolDefault("results.pretty", false) {
+               b, err = xml.MarshalIndent(r.obj, "", "  ")
+       } else {
+               b, err = xml.Marshal(r.obj)
+       }
+
+       if err != nil {
+               ErrorResult{Error: err}.Apply(req, resp)
+               return
+       }
+
+       resp.WriteHeader(http.StatusOK, "application/xml; charset=utf-8")
+       if _, err = resp.GetWriter().Write(b); err != nil {
+               resultsLog.Error("Apply: Response write failed", "error", err)
+       }
+}
+
+type RenderTextResult struct {
+       text string
+}
+
+func (r RenderTextResult) Apply(req *Request, resp *Response) {
+       resp.WriteHeader(http.StatusOK, "text/plain; charset=utf-8")
+       if _, err := resp.GetWriter().Write([]byte(r.text)); err != nil {
+               resultsLog.Error("Apply: Response write failed", "error", err)
+       }
+}
+
+type ContentDisposition string
+
+var (
+       NoDisposition ContentDisposition = ""
+       Attachment    ContentDisposition = "attachment"
+       Inline        ContentDisposition = "inline"
+)
+
+type BinaryResult struct {
+       Reader   io.Reader
+       Name     string
+       Length   int64
+       Delivery ContentDisposition
+       ModTime  time.Time
+}
+
+func (r *BinaryResult) Apply(req *Request, resp *Response) {
+       if r.Delivery != NoDisposition {
+               disposition := string(r.Delivery)
+               if r.Name != "" {
+                       disposition += fmt.Sprintf(`; filename="%s"`, r.Name)
+               }
+               resp.Out.internalHeader.Set("Content-Disposition", disposition)
+       }
+       if resp.ContentType != "" {
+               resp.Out.internalHeader.Set("Content-Type", resp.ContentType)
+       } else {
+               contentType := ContentTypeByFilename(r.Name)
+               resp.Out.internalHeader.Set("Content-Type", contentType)
+       }
+       if content, ok := r.Reader.(io.ReadSeeker); ok && r.Length < 0 {
+               // get the size from the stream
+               // go1.6 compatibility change, go1.6 does not define constants io.SeekStart
+               //if size, err := content.Seek(0, io.SeekEnd); err == nil {
+               //      if _, err = content.Seek(0, io.SeekStart); err == nil {
+               if size, err := content.Seek(0, 2); err == nil {
+                       if _, err = content.Seek(0, 0); err == nil {
+                               r.Length = size
+                       }
+               }
+       }
+
+       // Write stream writes the status code to the header as well
+       if ws := resp.GetStreamWriter(); ws != nil {
+               if err := ws.WriteStream(r.Name, r.Length, r.ModTime, r.Reader); err != nil {
+                       resultsLog.Error("Apply: Response write failed", "error", err)
+               }
+       }
+
+       // Close the Reader if we can
+       if v, ok := r.Reader.(io.Closer); ok {
+               _ = v.Close()
+       }
+}
+
+type RedirectToURLResult struct {
+       url string
+}
+
+func (r *RedirectToURLResult) Apply(req *Request, resp *Response) {
+       resp.Out.internalHeader.Set("Location", r.url)
+       resp.WriteHeader(http.StatusFound, "")
+}
+
+type RedirectToActionResult struct {
+       val  interface{}
+       args []interface{}
+}
+
+func (r *RedirectToActionResult) Apply(req *Request, resp *Response) {
+       url, err := getRedirectURL(r.val, r.args)
+       if err != nil {
+               resultsLog.Error("Apply: Couldn't resolve redirect", "error", err)
+               ErrorResult{Error: err}.Apply(req, resp)
+               return
+       }
+       resp.Out.internalHeader.Set("Location", url)
+       resp.WriteHeader(http.StatusFound, "")
+}
+
+func getRedirectURL(item interface{}, args []interface{}) (string, error) {
+       // Handle strings
+       if url, ok := item.(string); ok {
+               return url, nil
+       }
+
+       // Handle funcs
+       val := reflect.ValueOf(item)
+       typ := reflect.TypeOf(item)
+       if typ.Kind() == reflect.Func && typ.NumIn() > 0 {
+               // Get the Controller Method
+               recvType := typ.In(0)
+               method := FindMethod(recvType, val)
+               if method == nil {
+                       return "", errors.New("couldn't find method")
+               }
+
+               // Construct the action string (e.g. "Controller.Method")
+               if recvType.Kind() == reflect.Ptr {
+                       recvType = recvType.Elem()
+               }
+               module := ModuleFromPath(recvType.PkgPath(), true)
+               action := module.Namespace() + recvType.Name() + "." + method.Name
+               // Fetch the action path to get the defaults
+               pathData, found := splitActionPath(nil, action, true)
+               if !found {
+                       return "", fmt.Errorf("Unable to redirect '%s', expected 'Controller.Action'", action)
+               }
+
+               // Build the map for the router to reverse
+               // Unbind the arguments.
+               argsByName := make(map[string]string)
+               // Bind any static args first
+               fixedParams := len(pathData.FixedParamsByName)
+               methodType := pathData.TypeOfController.Method(pathData.MethodName)
+
+               for i, argValue := range args {
+                       Unbind(argsByName, methodType.Args[i+fixedParams].Name, argValue)
+               }
+
+               actionDef := MainRouter.Reverse(action, argsByName)
+               if actionDef == nil {
+                       return "", errors.New("no route for action " + action)
+               }
+
+               return actionDef.String(), nil
+       }
+
+       // Out of guesses
+       return "", errors.New("didn't recognize type: " + typ.String())
+}
diff --git a/src/foundation/api/revel/results_test.go b/src/foundation/api/revel/results_test.go
new file mode 100644 (file)
index 0000000..ce375d1
--- /dev/null
@@ -0,0 +1,73 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "fmt"
+       "net/http/httptest"
+       "strings"
+       "testing"
+)
+
+// Added test case for redirection testing for strings
+func TestRedirect(t *testing.T) {
+       startFakeBookingApp()
+       redirect := RedirectToURLResult{fmt.Sprintf("/hotels/index/foo")}
+       resp := httptest.NewRecorder()
+       c := NewTestController(resp, showRequest)
+       redirect.Apply(c.Request, c.Response)
+       if resp.Header().Get("Location") != "/hotels/index/foo" {
+               t.Errorf("Failed to set redirect header correctly. : %s", resp.Header().Get("Location"))
+       }
+}
+
+// Test that the render response is as expected.
+func TestBenchmarkRender(t *testing.T) {
+       startFakeBookingApp()
+       resp := httptest.NewRecorder()
+       c := NewTestController(resp, showRequest)
+       if err := c.SetAction("Hotels", "Show"); err != nil {
+               t.Errorf("SetAction failed: %s", err)
+       }
+       result := Hotels{c}.Show(3)
+       result.Apply(c.Request, c.Response)
+       if !strings.Contains(resp.Body.String(), "300 Main St.") {
+               t.Errorf("Failed to find hotel address in action response:\n%s", resp.Body)
+       }
+}
+
+func BenchmarkRenderChunked(b *testing.B) {
+       startFakeBookingApp()
+       resp := httptest.NewRecorder()
+       resp.Body = nil
+       c := NewTestController(resp, showRequest)
+       if err := c.SetAction("Hotels", "Show"); err != nil {
+               b.Errorf("SetAction failed: %s", err)
+       }
+       Config.SetOption("results.chunked", "true")
+       b.ResetTimer()
+
+       hotels := Hotels{c}
+       for i := 0; i < b.N; i++ {
+               hotels.Show(3).Apply(c.Request, c.Response)
+       }
+}
+
+func BenchmarkRenderNotChunked(b *testing.B) {
+       startFakeBookingApp()
+       resp := httptest.NewRecorder()
+       resp.Body = nil
+       c := NewTestController(resp, showRequest)
+       if err := c.SetAction("Hotels", "Show"); err != nil {
+               b.Errorf("SetAction failed: %s", err)
+       }
+       Config.SetOption("results.chunked", "false")
+       b.ResetTimer()
+
+       hotels := Hotels{c}
+       for i := 0; i < b.N; i++ {
+               hotels.Show(3).Apply(c.Request, c.Response)
+       }
+}
diff --git a/src/foundation/api/revel/revel.go b/src/foundation/api/revel/revel.go
new file mode 100644 (file)
index 0000000..27ac6ef
--- /dev/null
@@ -0,0 +1,301 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "go/build"
+       "log"
+       "path/filepath"
+       "strings"
+
+       "encoding/json"
+       "fmt"
+       "github.com/revel/config"
+       "github.com/revel/revel/logger"
+       "github.com/revel/revel/model"
+)
+
+const (
+       // RevelImportPath Revel framework import path
+       RevelImportPath = "github.com/revel/revel"
+)
+
+const (
+       TEST_MODE_FLAG   = "testModeFlag"
+       SPECIAL_USE_FLAG = "specialUseFlag"
+)
+
+// App details
+var (
+       RevelConfig *model.RevelContainer
+       AppName    string // e.g. "sample"
+       AppRoot    string // e.g. "/app1"
+       BasePath   string // e.g. "$GOPATH/src/corp/sample"
+       AppPath    string // e.g. "$GOPATH/src/corp/sample/app"
+       ViewsPath  string // e.g. "$GOPATH/src/corp/sample/app/views"
+       ImportPath string // e.g. "corp/sample"
+       SourcePath string // e.g. "$GOPATH/src"
+
+       Config  *config.Context
+       RunMode string // Application-defined (by default, "dev" or "prod")
+       DevMode bool   // if true, RunMode is a development mode.
+
+       // Revel installation details
+       RevelPath string // e.g. "$GOPATH/src/github.com/revel/revel"
+
+       // Where to look for templates
+       // Ordered by priority. (Earlier paths take precedence over later paths.)
+       CodePaths     []string // Code base directories, for modules and app
+       TemplatePaths []string // Template path directories manually added
+
+       // ConfPaths where to look for configurations
+       // Config load order
+       // 1. framework (revel/conf/*)
+       // 2. application (conf/*)
+       // 3. user supplied configs (...) - User configs can override/add any from above
+       ConfPaths []string
+
+       // Server config.
+       //
+       // Alert: This is how the app is configured, which may be different from
+       // the current process reality.  For example, if the app is configured for
+       // port 9000, HTTPPort will always be 9000, even though in dev mode it is
+       // run on a random port and proxied.
+       HTTPPort    int    // e.g. 9000
+       HTTPAddr    string // e.g. "", "127.0.0.1"
+       HTTPSsl     bool   // e.g. true if using ssl
+       HTTPSslCert string // e.g. "/path/to/cert.pem"
+       HTTPSslKey  string // e.g. "/path/to/key.pem"
+
+       // All cookies dropped by the framework begin with this prefix.
+       CookiePrefix string
+       // Cookie domain
+       CookieDomain string
+       // Cookie flags
+       CookieSecure bool
+
+       // Revel request access log, not exposed from package.
+       // However output settings can be controlled from app.conf
+
+       // True when revel engine has been initialized (Init has returned)
+       Initialized bool
+
+       // Private
+       secretKey     []byte             // Key used to sign cookies. An empty key disables signing.
+       packaged      bool               // If true, this is running from a pre-built package.
+       initEventList = []EventHandler{} // Event handler list for receiving events
+)
+
+// Init initializes Revel -- it provides paths for getting around the app.
+//
+// Params:
+//   mode - the run mode, which determines which app.conf settings are used.
+//   importPath - the Go import path of the application.
+//   srcPath - the path to the source directory, containing Revel and the app.
+//     If not specified (""), then a functioning Go installation is required.
+func Init(inputmode, importPath, srcPath string) {
+       RevelConfig = &model.RevelContainer{}
+       // Ignore trailing slashes.
+       ImportPath = strings.TrimRight(importPath, "/")
+       SourcePath = srcPath
+
+       RunMode = updateLog(inputmode)
+
+       // If the SourcePath is not specified, find it using build.Import.
+       var revelSourcePath string // may be different from the app source path
+       if SourcePath == "" {
+               revelSourcePath, SourcePath = findSrcPaths(importPath)
+       } else {
+               // If the SourcePath was specified, assume both Revel and the app are within it.
+               SourcePath = filepath.Clean(SourcePath)
+               revelSourcePath = SourcePath
+               packaged = true
+       }
+
+       RevelPath = filepath.Join(revelSourcePath, filepath.FromSlash(RevelImportPath))
+       BasePath = filepath.Join(SourcePath, filepath.FromSlash(importPath))
+       AppPath = filepath.Join(BasePath, "app")
+       ViewsPath = filepath.Join(AppPath, "views")
+
+       CodePaths = []string{AppPath}
+
+       if ConfPaths == nil {
+               ConfPaths = []string{}
+       }
+
+       // Config load order
+       // 1. framework (revel/conf/*)
+       // 2. application (conf/*)
+       // 3. user supplied configs (...) - User configs can override/add any from above
+       ConfPaths = append(
+               []string{
+                       filepath.Join(RevelPath, "conf"),
+                       filepath.Join(BasePath, "conf"),
+               },
+               ConfPaths...)
+
+       TemplatePaths = []string{
+               ViewsPath,
+               filepath.Join(RevelPath, "templates"),
+       }
+
+       // Load app.conf
+       var err error
+       Config, err = config.LoadContext("app.conf", ConfPaths)
+       if err != nil || Config == nil {
+               RevelLog.Fatal("Failed to load app.conf:", "error", err)
+       }
+
+       // After application config is loaded update the logger
+       updateLog(inputmode)
+
+       // Configure properties from app.conf
+       DevMode = Config.BoolDefault("mode.dev", false)
+       HTTPPort = Config.IntDefault("http.port", 9000)
+       HTTPAddr = Config.StringDefault("http.addr", "")
+       HTTPSsl = Config.BoolDefault("http.ssl", false)
+       HTTPSslCert = Config.StringDefault("http.sslcert", "")
+       HTTPSslKey = Config.StringDefault("http.sslkey", "")
+       if HTTPSsl {
+               if HTTPSslCert == "" {
+                       RevelLog.Fatal("No http.sslcert provided.")
+               }
+               if HTTPSslKey == "" {
+                       RevelLog.Fatal("No http.sslkey provided.")
+               }
+       }
+
+       AppName = Config.StringDefault("app.name", "(not set)")
+       AppRoot = Config.StringDefault("app.root", "")
+       CookiePrefix = Config.StringDefault("cookie.prefix", "REVEL")
+       CookieDomain = Config.StringDefault("cookie.domain", "")
+       CookieSecure = Config.BoolDefault("cookie.secure", HTTPSsl)
+       if secretStr := Config.StringDefault("app.secret", ""); secretStr != "" {
+               SetSecretKey([]byte(secretStr))
+       }
+
+       RaiseEvent(REVEL_BEFORE_MODULES_LOADED, nil)
+       loadModules()
+       RaiseEvent(REVEL_AFTER_MODULES_LOADED, nil)
+
+       Initialized = true
+       RevelLog.Info("Initialized Revel", "Version", Version, "BuildDate", BuildDate, "MinimumGoVersion", MinimumGoVersion)
+}
+
+// The input mode can be as simple as "prod" or it can be a JSON string like
+// {"mode":"%s","testModeFlag":true}
+// When this function is called it returns the true "inputmode" extracted from the parameter
+// and it sets the log context appropriately
+func updateLog(inputmode string) (returnMode string) {
+       if inputmode == "" {
+               returnMode = config.DefaultSection
+               return
+       } else {
+               returnMode = inputmode
+       }
+
+       // Check to see if the mode is a json object
+       modemap := map[string]interface{}{}
+
+       var testModeFlag, specialUseFlag bool
+       if err := json.Unmarshal([]byte(inputmode), &modemap); err == nil {
+               returnMode = modemap["mode"].(string)
+               if testmode, found := modemap[TEST_MODE_FLAG]; found {
+                       testModeFlag, _ = testmode.(bool)
+               }
+               if specialUse, found := modemap[SPECIAL_USE_FLAG]; found {
+                       specialUseFlag, _ = specialUse.(bool)
+               }
+       }
+
+       var newContext *config.Context
+       // If the Config is nil, set the logger to minimal log messages by adding the option
+       if Config == nil {
+               newContext = config.NewContext()
+               newContext.SetOption(TEST_MODE_FLAG, fmt.Sprint(true))
+       } else {
+               // Ensure that the selected runmode appears in app.conf.
+               // If empty string is passed as the mode, treat it as "DEFAULT"
+               if !Config.HasSection(returnMode) {
+                       log.Fatalln("app.conf: No mode found:", returnMode)
+               }
+               Config.SetSection(returnMode)
+               newContext = Config
+       }
+
+       // Only set the testmode flag if it doesnt exist
+       if _, found := newContext.Bool(TEST_MODE_FLAG); !found {
+               newContext.SetOption(TEST_MODE_FLAG, fmt.Sprint(testModeFlag))
+       }
+       if _, found := newContext.Bool(SPECIAL_USE_FLAG); !found {
+               newContext.SetOption(SPECIAL_USE_FLAG, fmt.Sprint(specialUseFlag))
+       }
+
+       appHandle := logger.InitializeFromConfig(BasePath, newContext)
+
+       // Set all the log handlers
+       setAppLog(AppLog, appHandle)
+
+       return
+}
+
+// Set the secret key
+func SetSecretKey(newKey []byte) error {
+       secretKey = newKey
+       return nil
+}
+
+// ResolveImportPath returns the filesystem path for the given import path.
+// Returns an error if the import path could not be found.
+func ResolveImportPath(importPath string) (string, error) {
+       if packaged {
+               return filepath.Join(SourcePath, importPath), nil
+       }
+
+       modPkg, err := build.Import(importPath, RevelPath, build.FindOnly)
+       if err != nil {
+               return "", err
+       }
+       return modPkg.Dir, nil
+}
+
+// CheckInit method checks `revel.Initialized` if not initialized it panics
+func CheckInit() {
+       if !Initialized {
+               RevelLog.Panic("CheckInit: Revel has not been initialized!")
+       }
+}
+
+// findSrcPaths uses the "go/build" package to find the source root for Revel
+// and the app.
+func findSrcPaths(importPath string) (revelSourcePath, appSourcePath string) {
+       var (
+               gopaths = filepath.SplitList(build.Default.GOPATH)
+               goroot  = build.Default.GOROOT
+       )
+
+       if len(gopaths) == 0 {
+               RevelLog.Fatal("GOPATH environment variable is not set. " +
+                       "Please refer to http://golang.org/doc/code.html to configure your Go environment.")
+       }
+
+       if ContainsString(gopaths, goroot) {
+               RevelLog.Fatalf("GOPATH (%s) must not include your GOROOT (%s). "+
+                       "Please refer to http://golang.org/doc/code.html to configure your Go environment.",
+                       gopaths, goroot)
+       }
+
+       appPkg, err := build.Import(importPath, "", build.FindOnly)
+       if err != nil {
+               RevelLog.Panic("Failed to import "+importPath+" with error:", "error", err)
+       }
+
+       revelPkg, err := build.Import(RevelImportPath, appPkg.Dir, build.FindOnly)
+       if err != nil {
+               RevelLog.Fatal("Failed to find Revel with error:", "error", err)
+       }
+
+       return revelPkg.Dir[:len(revelPkg.Dir)-len(RevelImportPath)], appPkg.SrcRoot
+}
diff --git a/src/foundation/api/revel/revel_hooks.go b/src/foundation/api/revel/revel_hooks.go
new file mode 100644 (file)
index 0000000..9378166
--- /dev/null
@@ -0,0 +1,103 @@
+package revel
+
+import (
+       "sort"
+)
+
+// The list of startup hooks
+type (
+       // The startup hooks structure
+       RevelHook struct {
+               order int    // The order
+               f     func() // The function to call
+       }
+
+       RevelHooks []RevelHook
+)
+
+var (
+       // The local instance of the list
+       startupHooks RevelHooks
+
+       // The instance of the list
+       shutdownHooks RevelHooks
+)
+
+// Called to run the hooks
+func (r RevelHooks) Run() {
+       serverLogger.Infof("There is %d hooks need to run ...", len(r))
+       sort.Sort(r)
+       for i, hook := range r {
+               utilLog.Infof("Run the %d hook ...", i+1)
+               hook.f()
+       }
+}
+
+// Adds a new function hook, using the order
+func (r RevelHooks) Add(fn func(), order ...int) RevelHooks {
+       o := 1
+       if len(order) > 0 {
+               o = order[0]
+       }
+       return append(r, RevelHook{order: o, f: fn})
+}
+
+// Sorting function
+func (slice RevelHooks) Len() int {
+       return len(slice)
+}
+
+// Sorting function
+func (slice RevelHooks) Less(i, j int) bool {
+       return slice[i].order < slice[j].order
+}
+
+// Sorting function
+func (slice RevelHooks) Swap(i, j int) {
+       slice[i], slice[j] = slice[j], slice[i]
+}
+
+// OnAppStart registers a function to be run at app startup.
+//
+// The order you register the functions will be the order they are run.
+// You can think of it as a FIFO queue.
+// This process will happen after the config file is read
+// and before the server is listening for connections.
+//
+// Ideally, your application should have only one call to init() in the file init.go.
+// The reason being that the call order of multiple init() functions in
+// the same package is undefined.
+// Inside of init() call revel.OnAppStart() for each function you wish to register.
+//
+// Example:
+//
+//      // from: yourapp/app/controllers/somefile.go
+//      func InitDB() {
+//          // do DB connection stuff here
+//      }
+//
+//      func FillCache() {
+//          // fill a cache from DB
+//          // this depends on InitDB having been run
+//      }
+//
+//      // from: yourapp/app/init.go
+//      func init() {
+//          // set up filters...
+//
+//          // register startup functions
+//          revel.OnAppStart(InitDB)
+//          revel.OnAppStart(FillCache)
+//      }
+//
+// This can be useful when you need to establish connections to databases or third-party services,
+// setup app components, compile assets, or any thing you need to do between starting Revel and accepting connections.
+//
+func OnAppStart(f func(), order ...int) {
+       startupHooks = startupHooks.Add(f, order...)
+}
+
+// Called to add a new stop hook
+func OnAppStop(f func(), order ...int) {
+       shutdownHooks = shutdownHooks.Add(f, order...)
+}
diff --git a/src/foundation/api/revel/revel_test.go b/src/foundation/api/revel/revel_test.go
new file mode 100644 (file)
index 0000000..1f8c87a
--- /dev/null
@@ -0,0 +1,13 @@
+package revel
+
+import (
+       "net/http"
+)
+
+func NewTestController(w http.ResponseWriter, r *http.Request) *Controller {
+       context := NewGoContext(nil)
+       context.Request.SetRequest(r)
+       context.Response.SetResponse(w)
+       c := NewController(context)
+       return c
+}
diff --git a/src/foundation/api/revel/router.go b/src/foundation/api/revel/router.go
new file mode 100644 (file)
index 0000000..54fb6e2
--- /dev/null
@@ -0,0 +1,846 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "encoding/csv"
+       "fmt"
+       "io"
+       "io/ioutil"
+       "net/url"
+       "path/filepath"
+       "regexp"
+       "strings"
+
+       "os"
+       "sync"
+
+       "github.com/revel/pathtree"
+       "github.com/revel/revel/logger"
+)
+
+const (
+       httpStatusCode = "404"
+)
+
+type Route struct {
+       ModuleSource        *Module         // Module name of route
+       Method              string          // e.g. GET
+       Path                string          // e.g. /app/:id
+       Action              string          // e.g. "Application.ShowApp", "404"
+       ControllerNamespace string          // e.g. "testmodule.",
+       ControllerName      string          // e.g. "Application", ""
+       MethodName          string          // e.g. "ShowApp", ""
+       FixedParams         []string        // e.g. "arg1","arg2","arg3" (CSV formatting)
+       TreePath            string          // e.g. "/GET/app/:id"
+       TypeOfController    *ControllerType // The controller type (if route is not wild carded)
+
+       routesPath string // e.g. /Users/robfig/gocode/src/myapp/conf/routes
+       line       int    // e.g. 3
+}
+
+type RouteMatch struct {
+       Action           string // e.g. 404
+       ControllerName   string // e.g. Application
+       MethodName       string // e.g. ShowApp
+       FixedParams      []string
+       Params           map[string][]string // e.g. {id: 123}
+       TypeOfController *ControllerType     // The controller type
+       ModuleSource     *Module             // The module
+}
+
+type ActionPathData struct {
+       Key                 string            // The unique key
+       ControllerNamespace string            // The controller namespace
+       ControllerName      string            // The controller name
+       MethodName          string            // The method name
+       Action              string            // The action
+       ModuleSource        *Module           // The module
+       Route               *Route            // The route
+       FixedParamsByName   map[string]string // The fixed parameters
+       TypeOfController    *ControllerType   // The controller type
+}
+
+var (
+       // Used to store decoded action path mappings
+       actionPathCacheMap = map[string]*ActionPathData{}
+       // Used to prevent concurrent writes to map
+       actionPathCacheLock = sync.Mutex{}
+       // The path returned if not found
+       notFound = &RouteMatch{Action: "404"}
+)
+
+var routerLog = RevelLog.New("section", "router")
+
+func init() {
+       AddInitEventHandler(func(typeOf Event, value interface{}) (responseOf EventResponse) {
+               // Add in an
+               if typeOf == ROUTE_REFRESH_REQUESTED {
+                       // Clear the actionPathCacheMap cache
+                       actionPathCacheLock.Lock()
+                       defer actionPathCacheLock.Unlock()
+                       actionPathCacheMap = map[string]*ActionPathData{}
+               }
+               return
+       })
+}
+
+// NewRoute prepares the route to be used in matching.
+func NewRoute(moduleSource *Module, method, path, action, fixedArgs, routesPath string, line int) (r *Route) {
+       // Handle fixed arguments
+       argsReader := strings.NewReader(string(namespaceReplace([]byte(fixedArgs), moduleSource)))
+       csvReader := csv.NewReader(argsReader)
+       csvReader.TrimLeadingSpace = true
+       fargs, err := csvReader.Read()
+       if err != nil && err != io.EOF {
+               routerLog.Error("NewRoute: Invalid fixed parameters for string ", "error", err, "fixedargs", fixedArgs)
+       }
+
+       r = &Route{
+               ModuleSource: moduleSource,
+               Method:       strings.ToUpper(method),
+               Path:         path,
+               Action:       string(namespaceReplace([]byte(action), moduleSource)),
+               FixedParams:  fargs,
+               TreePath:     treePath(strings.ToUpper(method), path),
+               routesPath:   routesPath,
+               line:         line,
+       }
+
+       // URL pattern
+       if !strings.HasPrefix(r.Path, "/") {
+               routerLog.Error("NewRoute: Absolute URL required.")
+               return
+       }
+
+       // Ignore the not found status code
+       if action != httpStatusCode {
+               routerLog.Debugf("NewRoute: New splitActionPath path:%s action:%s", path, action)
+               pathData, found := splitActionPath(&ActionPathData{ModuleSource: moduleSource, Route: r}, r.Action, false)
+               if found {
+                       if pathData.TypeOfController != nil {
+                               // Assign controller type to avoid looking it up based on name
+                               r.TypeOfController = pathData.TypeOfController
+                               // Create the fixed parameters
+                               if l := len(pathData.Route.FixedParams); l > 0 && len(pathData.FixedParamsByName) == 0 {
+                                       methodType := pathData.TypeOfController.Method(pathData.MethodName)
+                                       if methodType != nil {
+                                               pathData.FixedParamsByName = make(map[string]string, l)
+                                               for i, argValue := range pathData.Route.FixedParams {
+                                                       Unbind(pathData.FixedParamsByName, methodType.Args[i].Name, argValue)
+                                               }
+                                       } else {
+                                               routerLog.Panicf("NewRoute: Method %s not found for controller %s", pathData.MethodName, pathData.ControllerName)
+                                       }
+                               }
+                       }
+                       r.ControllerNamespace = pathData.ControllerNamespace
+                       r.ControllerName = pathData.ControllerName
+                       r.ModuleSource = pathData.ModuleSource
+                       r.MethodName = pathData.MethodName
+
+                       // The same action path could be used for multiple routes (like the Static.Serve)
+               } else {
+                       routerLog.Panicf("NewRoute: Failed to find controller for route path action %s \n%#v\n", path+"?"+r.Action, actionPathCacheMap)
+               }
+       }
+       return
+}
+
+func (route *Route) ActionPath() string {
+       return route.ModuleSource.Namespace() + route.ControllerName
+}
+
+func treePath(method, path string) string {
+       if method == "*" {
+               method = ":METHOD"
+       }
+       return "/" + method + path
+}
+
+type Router struct {
+       Routes []*Route
+       Tree   *pathtree.Node
+       Module string // The module the route is associated with
+       path   string // path to the routes file
+}
+
+func (router *Router) Route(req *Request) (routeMatch *RouteMatch) {
+       // Override method if set in header
+       if method := req.GetHttpHeader("X-HTTP-Method-Override"); method != "" && req.Method == "POST" {
+               req.Method = method
+       }
+
+       leaf, expansions := router.Tree.Find(treePath(req.Method, req.GetPath()))
+       if leaf == nil {
+               return nil
+       }
+
+       // Create a map of the route parameters.
+       var params url.Values
+       if len(expansions) > 0 {
+               params = make(url.Values)
+               for i, v := range expansions {
+                       params[leaf.Wildcards[i]] = []string{v}
+               }
+       }
+       var route *Route
+       var controllerName, methodName string
+
+       // The leaf value is now a list of possible routes to match, only a controller
+       routeList := leaf.Value.([]*Route)
+       var typeOfController *ControllerType
+
+       //INFO.Printf("Found route for path %s %#v", req.URL.Path, len(routeList))
+       for index := range routeList {
+               route = routeList[index]
+               methodName = route.MethodName
+
+               // Special handling for explicit 404's.
+               if route.Action == httpStatusCode {
+                       route = nil
+                       break
+               }
+
+               // If wildcard match on method name use the method name from the params
+               if methodName[0] == ':' {
+                       if methodKey, found := params[methodName[1:]]; found && len(methodKey) > 0 {
+                               methodName = strings.ToLower(methodKey[0])
+                       } else {
+                               routerLog.Fatal("Route: Failure to find method name in parameters", "params", params, "methodName", methodName)
+                       }
+               }
+
+               // If the action is variablized, replace into it with the captured args.
+               controllerName = route.ControllerName
+               if controllerName[0] == ':' {
+                       controllerName = strings.ToLower(params[controllerName[1:]][0])
+                       if typeOfController = route.ModuleSource.ControllerByName(controllerName, methodName); typeOfController != nil {
+                               break
+                       }
+               } else {
+                       typeOfController = route.TypeOfController
+                       break
+               }
+               route = nil
+       }
+
+       if route == nil {
+               routeMatch = notFound
+       } else {
+
+               routeMatch = &RouteMatch{
+                       ControllerName:   route.ControllerNamespace + controllerName,
+                       MethodName:       methodName,
+                       Params:           params,
+                       FixedParams:      route.FixedParams,
+                       TypeOfController: typeOfController,
+                       ModuleSource:     route.ModuleSource,
+               }
+       }
+
+       return
+}
+
+// Refresh re-reads the routes file and re-calculates the routing table.
+// Returns an error if a specified action could not be found.
+func (router *Router) Refresh() (err *Error) {
+       RaiseEvent(ROUTE_REFRESH_REQUESTED, nil)
+       router.Routes, err = parseRoutesFile(appModule, router.path, "", true)
+       RaiseEvent(ROUTE_REFRESH_COMPLETED, nil)
+       if err != nil {
+               return
+       }
+       err = router.updateTree()
+       return
+}
+
+func (router *Router) updateTree() *Error {
+       router.Tree = pathtree.New()
+       pathMap := map[string][]*Route{}
+
+       allPathsOrdered := []string{}
+       // It is possible for some route paths to overlap
+       // based on wildcard matches,
+       // TODO when pathtree is fixed (made to be smart enough to not require a predefined intake order) keeping the routes in order is not necessary
+       for _, route := range router.Routes {
+               if _, found := pathMap[route.TreePath]; !found {
+                       pathMap[route.TreePath] = append(pathMap[route.TreePath], route)
+                       allPathsOrdered = append(allPathsOrdered, route.TreePath)
+               } else {
+                       pathMap[route.TreePath] = append(pathMap[route.TreePath], route)
+               }
+       }
+       for _, path := range allPathsOrdered {
+               routeList := pathMap[path]
+               err := router.Tree.Add(path, routeList)
+
+               // Allow GETs to respond to HEAD requests.
+               if err == nil && routeList[0].Method == "GET" {
+                       err = router.Tree.Add(treePath("HEAD", routeList[0].Path), routeList)
+               }
+
+               // Error adding a route to the pathtree.
+               if err != nil {
+                       return routeError(err, path, fmt.Sprintf("%#v", routeList), routeList[0].line)
+               }
+       }
+       return nil
+}
+
+// Returns the controller namespace and name, action and module if found from the actionPath specified
+func splitActionPath(actionPathData *ActionPathData, actionPath string, useCache bool) (pathData *ActionPathData, found bool) {
+       actionPath = strings.ToLower(actionPath)
+       if pathData, found = actionPathCacheMap[actionPath]; found && useCache {
+               return
+       }
+       var (
+               controllerNamespace, controllerName, methodName, action string
+               foundModuleSource                                       *Module
+               typeOfController                                        *ControllerType
+               log                                                     = routerLog.New("actionPath", actionPath)
+       )
+       actionSplit := strings.Split(actionPath, ".")
+       if actionPathData != nil {
+               foundModuleSource = actionPathData.ModuleSource
+       }
+       if len(actionSplit) == 2 {
+               controllerName, methodName = strings.ToLower(actionSplit[0]), strings.ToLower(actionSplit[1])
+               if i := strings.Index(methodName, "("); i > 0 {
+                       methodName = methodName[:i]
+               }
+               log = log.New("controller", controllerName, "method", methodName)
+               log.Debug("splitActionPath: Check for namespace")
+               if i := strings.Index(controllerName, namespaceSeperator); i > -1 {
+                       controllerNamespace = controllerName[:i+1]
+                       if moduleSource, found := ModuleByName(controllerNamespace[:len(controllerNamespace)-1]); found {
+                               log.Debug("Found module namespace")
+                               foundModuleSource = moduleSource
+                               controllerNamespace = moduleSource.Namespace()
+                       } else {
+                               log.Warnf("splitActionPath: Unable to find module %s for action: %s", controllerNamespace[:len(controllerNamespace)-1], actionPath)
+                       }
+                       controllerName = controllerName[i+1:]
+                       // Check for the type of controller
+                       typeOfController = foundModuleSource.ControllerByName(controllerName, methodName)
+                       found = typeOfController != nil
+               } else if controllerName[0] != ':' {
+                       // First attempt to find the controller in the module source
+                       if foundModuleSource != nil {
+                               typeOfController = foundModuleSource.ControllerByName(controllerName, methodName)
+                               if typeOfController != nil {
+                                       controllerNamespace = typeOfController.Namespace
+                               }
+                       }
+                       log.Info("Found controller for path", "controllerType", typeOfController)
+
+                       if typeOfController == nil {
+                               // Check to see if we can determine the controller from only the controller name
+                               // an actionPath without a moduleSource will only come from
+                               // Scan through the controllers
+                               matchName := controllerName
+                               for key, controller := range controllers {
+                                       // Strip away the namespace from the controller. to be match
+                                       regularName := key
+                                       if i := strings.Index(key, namespaceSeperator); i > -1 {
+                                               regularName = regularName[i+1:]
+                                       }
+                                       if regularName == matchName {
+                                               // Found controller match
+                                               typeOfController = controller
+                                               controllerNamespace = typeOfController.Namespace
+                                               controllerName = typeOfController.ShortName()
+                                               foundModuleSource = typeOfController.ModuleSource
+                                               found = true
+                                               break
+                                       }
+                               }
+                       } else {
+                               found = true
+                       }
+               } else {
+                       // If wildcard assign the route the controller namespace found
+                       controllerNamespace = actionPathData.ModuleSource.Name + namespaceSeperator
+                       foundModuleSource = actionPathData.ModuleSource
+                       found = true
+               }
+               action = actionSplit[1]
+       } else {
+               foundPaths := ""
+               for path := range actionPathCacheMap {
+                       foundPaths += path + ","
+               }
+               log.Warnf("splitActionPath: Invalid action path %s found paths %s", actionPath, foundPaths)
+               found = false
+       }
+
+       // Make sure no concurrent map writes occur
+       if found {
+               actionPathCacheLock.Lock()
+               defer actionPathCacheLock.Unlock()
+               if actionPathData != nil {
+                       actionPathData.ControllerNamespace = controllerNamespace
+                       actionPathData.ControllerName = controllerName
+                       actionPathData.MethodName = methodName
+                       actionPathData.Action = action
+                       actionPathData.ModuleSource = foundModuleSource
+                       actionPathData.TypeOfController = typeOfController
+               } else {
+                       actionPathData = &ActionPathData{
+                               ControllerNamespace: controllerNamespace,
+                               ControllerName:      controllerName,
+                               MethodName:          methodName,
+                               Action:              action,
+                               ModuleSource:        foundModuleSource,
+                               TypeOfController:    typeOfController,
+                       }
+               }
+               actionPathData.TypeOfController = foundModuleSource.ControllerByName(controllerName, "")
+               if actionPathData.TypeOfController == nil && actionPathData.ControllerName[0] != ':' {
+                       log.Warnf("splitActionPath: No controller found for %s %#v", foundModuleSource.Namespace()+controllerName, controllers)
+               }
+
+               pathData = actionPathData
+               if pathData.Route != nil && len(pathData.Route.FixedParams) > 0 {
+                       // If there are fixed params on the route then add them to the path
+                       // This will give it a unique path and it should still be usable for a reverse lookup provided the name is matchable
+                       // for example
+                       // GET   /test/                     Application.Index("Test", "Test2")
+                       // {{url "Application.Index(test,test)" }}
+                       // should be parseable
+                       actionPath = actionPath + "(" + strings.ToLower(strings.Join(pathData.Route.FixedParams, ",")) + ")"
+               }
+               if actionPathData.Route != nil {
+                       log.Debugf("splitActionPath: Split Storing recognized action path %s for route  %#v ", actionPath, actionPathData.Route)
+               }
+               pathData.Key = actionPath
+               actionPathCacheMap[actionPath] = pathData
+               if !strings.Contains(actionPath, namespaceSeperator) && pathData.TypeOfController != nil {
+                       actionPathCacheMap[strings.ToLower(pathData.TypeOfController.Namespace)+actionPath] = pathData
+                       log.Debugf("splitActionPath: Split Storing recognized action path %s for route  %#v ", strings.ToLower(pathData.TypeOfController.Namespace)+actionPath, actionPathData.Route)
+               }
+       }
+       return
+}
+
+// parseRoutesFile reads the given routes file and returns the contained routes.
+func parseRoutesFile(moduleSource *Module, routesPath, joinedPath string, validate bool) ([]*Route, *Error) {
+       contentBytes, err := ioutil.ReadFile(routesPath)
+       if err != nil {
+               return nil, &Error{
+                       Title:       "Failed to load routes file",
+                       Description: err.Error(),
+               }
+       }
+       return parseRoutes(moduleSource, routesPath, joinedPath, string(contentBytes), validate)
+}
+
+// parseRoutes reads the content of a routes file into the routing table.
+func parseRoutes(moduleSource *Module, routesPath, joinedPath, content string, validate bool) ([]*Route, *Error) {
+       var routes []*Route
+
+       // For each line..
+       for n, line := range strings.Split(content, "\n") {
+               line = strings.TrimSpace(line)
+               if len(line) == 0 || line[0] == '#' {
+                       continue
+               }
+
+               const modulePrefix = "module:"
+
+               // Handle included routes from modules.
+               // e.g. "module:testrunner" imports all routes from that module.
+               if strings.HasPrefix(line, modulePrefix) {
+                       moduleRoutes, err := getModuleRoutes(line[len(modulePrefix):], joinedPath, validate)
+                       if err != nil {
+                               return nil, routeError(err, routesPath, content, n)
+                       }
+                       routes = append(routes, moduleRoutes...)
+                       continue
+               }
+
+               // A single route
+               method, path, action, fixedArgs, found := parseRouteLine(line)
+               if !found {
+                       continue
+               }
+
+               // this will avoid accidental double forward slashes in a route.
+               // this also avoids pathtree freaking out and causing a runtime panic
+               // because of the double slashes
+               if strings.HasSuffix(joinedPath, "/") && strings.HasPrefix(path, "/") {
+                       joinedPath = joinedPath[0 : len(joinedPath)-1]
+               }
+               path = strings.Join([]string{AppRoot, joinedPath, path}, "")
+
+               // This will import the module routes under the path described in the
+               // routes file (joinedPath param). e.g. "* /jobs module:jobs" -> all
+               // routes' paths will have the path /jobs prepended to them.
+               // See #282 for more info
+               if method == "*" && strings.HasPrefix(action, modulePrefix) {
+                       moduleRoutes, err := getModuleRoutes(action[len(modulePrefix):], path, validate)
+                       if err != nil {
+                               return nil, routeError(err, routesPath, content, n)
+                       }
+                       routes = append(routes, moduleRoutes...)
+                       continue
+               }
+
+               route := NewRoute(moduleSource, method, path, action, fixedArgs, routesPath, n)
+               routes = append(routes, route)
+
+               if validate {
+                       if err := validateRoute(route); err != nil {
+                               return nil, routeError(err, routesPath, content, n)
+                       }
+               }
+       }
+
+       return routes, nil
+}
+
+// validateRoute checks that every specified action exists.
+func validateRoute(route *Route) error {
+       // Skip 404s
+       if route.Action == httpStatusCode {
+               return nil
+       }
+
+       // Skip variable routes.
+       if route.ControllerName[0] == ':' || route.MethodName[0] == ':' {
+               return nil
+       }
+
+       // Precheck to see if controller exists
+       if _, found := controllers[route.ControllerNamespace+route.ControllerName]; !found {
+               // Scan through controllers to find module
+               for _, c := range controllers {
+                       controllerName := strings.ToLower(c.Type.Name())
+                       if controllerName == route.ControllerName {
+                               route.ControllerNamespace = c.ModuleSource.Name + namespaceSeperator
+                               routerLog.Warn("validateRoute: Matched empty namespace route for %s to this namespace %s for the route %s", controllerName, c.ModuleSource.Name, route.Path)
+                       }
+               }
+       }
+
+       // TODO need to check later
+       // does it do only validation or validation and instantiate the controller.
+       var c Controller
+       return c.SetTypeAction(route.ControllerNamespace+route.ControllerName, route.MethodName, route.TypeOfController)
+}
+
+// routeError adds context to a simple error message.
+func routeError(err error, routesPath, content string, n int) *Error {
+       if revelError, ok := err.(*Error); ok {
+               return revelError
+       }
+       // Load the route file content if necessary
+       if content == "" {
+               if contentBytes, er := ioutil.ReadFile(routesPath); er != nil {
+                       routerLog.Error("routeError: Failed to read route file ", "file", routesPath, "error", er)
+               } else {
+                       content = string(contentBytes)
+               }
+       }
+       return &Error{
+               Title:       "Route validation error",
+               Description: err.Error(),
+               Path:        routesPath,
+               Line:        n + 1,
+               SourceLines: strings.Split(content, "\n"),
+               Stack:       fmt.Sprintf("%s", logger.NewCallStack()),
+       }
+}
+
+// getModuleRoutes loads the routes file for the given module and returns the
+// list of routes.
+func getModuleRoutes(moduleName, joinedPath string, validate bool) (routes []*Route, err *Error) {
+       // Look up the module.  It may be not found due to the common case of e.g. the
+       // testrunner module being active only in dev mode.
+       module, found := ModuleByName(moduleName)
+       if !found {
+               routerLog.Debug("getModuleRoutes: Skipping routes for inactive module", "module", moduleName)
+               return nil, nil
+       }
+       routePath := filepath.Join(module.Path, "conf", "routes")
+       if _, e := os.Stat(routePath); e == nil {
+               routes, err = parseRoutesFile(module, routePath, joinedPath, validate)
+       }
+       if err == nil {
+               for _, route := range routes {
+                       route.ModuleSource = module
+               }
+       }
+
+       return routes, err
+}
+
+// Groups:
+// 1: method
+// 4: path
+// 5: action
+// 6: fixedargs
+var routePattern = regexp.MustCompile(
+       "(?i)^(GET|POST|PUT|DELETE|PATCH|OPTIONS|HEAD|WS|PROPFIND|MKCOL|COPY|MOVE|PROPPATCH|LOCK|UNLOCK|TRACE|PURGE|\\*)" +
+               "[(]?([^)]*)(\\))?[ \t]+" +
+               "(.*/[^ \t]*)[ \t]+([^ \t(]+)" +
+               `\(?([^)]*)\)?[ \t]*$`)
+
+func parseRouteLine(line string) (method, path, action, fixedArgs string, found bool) {
+       matches := routePattern.FindStringSubmatch(line)
+       if matches == nil {
+               return
+       }
+       method, path, action, fixedArgs = matches[1], matches[4], matches[5], matches[6]
+       found = true
+       return
+}
+
+func NewRouter(routesPath string) *Router {
+       return &Router{
+               Tree: pathtree.New(),
+               path: routesPath,
+       }
+}
+
+type ActionDefinition struct {
+       Host, Method, URL, Action string
+       Star                      bool
+       Args                      map[string]string
+}
+
+func (a *ActionDefinition) String() string {
+       return a.URL
+}
+
+func (router *Router) Reverse(action string, argValues map[string]string) (ad *ActionDefinition) {
+       log := routerLog.New("action", action)
+       pathData, found := splitActionPath(nil, action, true)
+       if !found {
+               routerLog.Error("splitActionPath: Failed to find reverse route", "action", action, "arguments", argValues)
+               return nil
+       }
+
+       log.Debug("Checking for route", "pathdataRoute", pathData.Route)
+       if pathData.Route == nil {
+               var possibleRoute *Route
+               // If the route is nil then we need to go through the routes to find the first matching route
+               // from this controllers namespace, this is likely a wildcard route match
+               for _, route := range router.Routes {
+                       // Skip routes that are not wild card or empty
+                       if route.ControllerName == "" || route.MethodName == "" {
+                               continue
+                       }
+                       if route.ModuleSource == pathData.ModuleSource && route.ControllerName[0] == ':' {
+                               // Wildcard match in same module space
+                               pathData.Route = route
+                               break
+                       } else if route.ActionPath() == pathData.ModuleSource.Namespace()+pathData.ControllerName &&
+                               (route.Method[0] == ':' || route.Method == pathData.MethodName) {
+                               // Action path match
+                               pathData.Route = route
+                               break
+                       } else if route.ControllerName == pathData.ControllerName &&
+                               (route.Method[0] == ':' || route.Method == pathData.MethodName) {
+                               // Controller name match
+                               possibleRoute = route
+                       }
+               }
+               if pathData.Route == nil && possibleRoute != nil {
+                       pathData.Route = possibleRoute
+                       routerLog.Warnf("Reverse: For a url reverse a match was based on  %s matched path to route %#v ", action, possibleRoute)
+               }
+               if pathData.Route != nil {
+                       routerLog.Debugf("Reverse: Reverse Storing recognized action path %s for route %#v\n", action, pathData.Route)
+               }
+       }
+
+       // Likely unknown route because of a wildcard, perform manual lookup
+       if pathData.Route != nil {
+               route := pathData.Route
+
+               // If the controller or method are wildcards we need to populate the argValues
+               controllerWildcard := route.ControllerName[0] == ':'
+               methodWildcard := route.MethodName[0] == ':'
+
+               // populate route arguments with the names
+               if controllerWildcard {
+                       argValues[route.ControllerName[1:]] = pathData.ControllerName
+               }
+               if methodWildcard {
+                       argValues[route.MethodName[1:]] = pathData.MethodName
+               }
+               // In theory all routes should be defined and pre-populated, the route controllers may not be though
+               // with wildcard routes
+               if pathData.TypeOfController == nil {
+                       if controllerWildcard || methodWildcard {
+                               if controller := ControllerTypeByName(pathData.ControllerNamespace+pathData.ControllerName, route.ModuleSource); controller != nil {
+                                       // Wildcard match boundary
+                                       pathData.TypeOfController = controller
+                                       // See if the path exists in the module based
+                               } else {
+                                       routerLog.Errorf("Reverse: Controller %s not found in reverse lookup", pathData.ControllerNamespace+pathData.ControllerName)
+                                       return
+                               }
+                       }
+               }
+
+               if pathData.TypeOfController == nil {
+                       routerLog.Errorf("Reverse: Controller %s not found in reverse lookup", pathData.ControllerNamespace+pathData.ControllerName)
+                       return
+               }
+               var (
+                       queryValues  = make(url.Values)
+                       pathElements = strings.Split(route.Path, "/")
+               )
+               for i, el := range pathElements {
+                       if el == "" || (el[0] != ':' && el[0] != '*') {
+                               continue
+                       }
+                       val, ok := pathData.FixedParamsByName[el[1:]]
+                       if !ok {
+                               val, ok = argValues[el[1:]]
+                       }
+                       if !ok {
+                               val = "<nil>"
+                               routerLog.Error("Reverse: reverse route missing route argument ", "argument", el[1:])
+                       }
+                       pathElements[i] = val
+                       delete(argValues, el[1:])
+                       continue
+               }
+
+               // Add any args that were not inserted into the path into the query string.
+               for k, v := range argValues {
+                       queryValues.Set(k, v)
+               }
+
+               // Calculate the final URL and Method
+               urlPath := strings.Join(pathElements, "/")
+               if len(queryValues) > 0 {
+                       urlPath += "?" + queryValues.Encode()
+               }
+
+               method := route.Method
+               star := false
+               if route.Method == "*" {
+                       method = "GET"
+                       star = true
+               }
+
+               //INFO.Printf("Reversing action %s to %s Using Route %#v",action,url,pathData.Route)
+
+               return &ActionDefinition{
+                       URL:    urlPath,
+                       Method: method,
+                       Star:   star,
+                       Action: action,
+                       Args:   argValues,
+                       Host:   "TODO",
+               }
+       }
+
+       routerLog.Error("Reverse: Failed to find controller for reverse route", "action", action, "arguments", argValues)
+       return nil
+}
+
+func RouterFilter(c *Controller, fc []Filter) {
+       // Figure out the Controller/Action
+       route := MainRouter.Route(c.Request)
+       if route == nil {
+               c.Result = c.NotFound("No matching route found: " + c.Request.GetRequestURI())
+               return
+       }
+
+       // The route may want to explicitly return a 404.
+       if route.Action == httpStatusCode {
+               c.Result = c.NotFound("(intentionally)")
+               return
+       }
+
+       // Set the action.
+       if err := c.SetTypeAction(route.ControllerName, route.MethodName, route.TypeOfController); err != nil {
+               c.Result = c.NotFound(err.Error())
+               return
+       }
+
+       // Add the route and fixed params to the Request Params.
+       c.Params.Route = route.Params
+       // Assign logger if from module
+       if c.Type.ModuleSource != nil && c.Type.ModuleSource != appModule {
+               c.Log = c.Type.ModuleSource.Log.New("ip", c.ClientIP,
+                       "path", c.Request.URL.Path, "method", c.Request.Method)
+       }
+
+       // Add the fixed parameters mapped by name.
+       // TODO: Pre-calculate this mapping.
+       for i, value := range route.FixedParams {
+               if c.Params.Fixed == nil {
+                       c.Params.Fixed = make(url.Values)
+               }
+               if i < len(c.MethodType.Args) {
+                       arg := c.MethodType.Args[i]
+                       c.Params.Fixed.Set(arg.Name, value)
+               } else {
+                       routerLog.Warn("RouterFilter: Too many parameters to action", "action", route.Action, "value", value)
+                       break
+               }
+       }
+
+       fc[0](c, fc[1:])
+}
+
+// HTTPMethodOverride overrides allowed http methods via form or browser param
+func HTTPMethodOverride(c *Controller, fc []Filter) {
+       // An array of HTTP verbs allowed.
+       verbs := []string{"POST", "PUT", "PATCH", "DELETE"}
+
+       method := strings.ToUpper(c.Request.Method)
+
+       if method == "POST" {
+               param := ""
+               if f, err := c.Request.GetForm(); err == nil {
+                       param = strings.ToUpper(f.Get("_method"))
+               }
+
+               if len(param) > 0 {
+                       override := false
+                       // Check if param is allowed
+                       for _, verb := range verbs {
+                               if verb == param {
+                                       override = true
+                                       break
+                               }
+                       }
+
+                       if override {
+                               c.Request.Method = param
+                       } else {
+                               c.Response.Status = 405
+                               c.Result = c.RenderError(&Error{
+                                       Title:       "Method not allowed",
+                                       Description: "Method " + param + " is not allowed (valid: " + strings.Join(verbs, ", ") + ")",
+                               })
+                               return
+                       }
+
+               }
+       }
+
+       fc[0](c, fc[1:]) // Execute the next filter stage.
+}
+
+func init() {
+       OnAppStart(func() {
+               MainRouter = NewRouter(filepath.Join(BasePath, "conf", "routes"))
+               err := MainRouter.Refresh()
+               if MainWatcher != nil && Config.BoolDefault("watch.routes", true) {
+                       MainWatcher.Listen(MainRouter, MainRouter.path)
+               } else if err != nil {
+                       // Not in dev mode and Route loading failed, we should crash.
+                       routerLog.Panic("init: router initialize error", "error", err)
+               }
+       })
+}
diff --git a/src/foundation/api/revel/router_test.go b/src/foundation/api/revel/router_test.go
new file mode 100644 (file)
index 0000000..fe66970
--- /dev/null
@@ -0,0 +1,678 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "fmt"
+       "net/http"
+       "net/url"
+       "strings"
+       "testing"
+)
+
+// Data-driven tests that check that a given routes-file line translates into
+// the expected Route object.
+var routeTestCases = map[string]*Route{
+       "get / Application.Index": {
+               Method:      "GET",
+               Path:        "/",
+               Action:      "Application.Index",
+               FixedParams: []string{},
+       },
+
+       "post /app/:id Application.SaveApp": {
+               Method:      "POST",
+               Path:        "/app/:id",
+               Action:      "Application.SaveApp",
+               FixedParams: []string{},
+       },
+
+       "get /app/ Application.List": {
+               Method:      "GET",
+               Path:        "/app/",
+               Action:      "Application.List",
+               FixedParams: []string{},
+       },
+
+       `get /app/:appId/ Application.Show`: {
+               Method:      "GET",
+               Path:        `/app/:appId/`,
+               Action:      "Application.Show",
+               FixedParams: []string{},
+       },
+
+       `get /app-wild/*appId/ Application.WildShow`: {
+               Method:      "GET",
+               Path:        `/app-wild/*appId/`,
+               Action:      "Application.WildShow",
+               FixedParams: []string{},
+       },
+
+       `GET /public/:filepath   Static.Serve("public")`: {
+               Method: "GET",
+               Path:   "/public/:filepath",
+               Action: "Static.Serve",
+               FixedParams: []string{
+                       "public",
+               },
+       },
+
+       `GET /javascript/:filepath Static.Serve("public/js")`: {
+               Method: "GET",
+               Path:   "/javascript/:filepath",
+               Action: "Static.Serve",
+               FixedParams: []string{
+                       "public",
+               },
+       },
+
+       "* /apps/:id/:action Application.:action": {
+               Method:      "*",
+               Path:        "/apps/:id/:action",
+               Action:      "Application.:action",
+               FixedParams: []string{},
+       },
+
+       "* /:controller/:action :controller.:action": {
+               Method:      "*",
+               Path:        "/:controller/:action",
+               Action:      ":controller.:action",
+               FixedParams: []string{},
+       },
+
+       `GET / Application.Index("Test", "Test2")`: {
+               Method: "GET",
+               Path:   "/",
+               Action: "Application.Index",
+               FixedParams: []string{
+                       "Test",
+                       "Test2",
+               },
+       },
+}
+
+// Run the test cases above.
+func TestComputeRoute(t *testing.T) {
+       for routeLine, expected := range routeTestCases {
+               method, path, action, fixedArgs, found := parseRouteLine(routeLine)
+               if !found {
+                       t.Error("Failed to parse route line:", routeLine)
+                       continue
+               }
+               actual := NewRoute(appModule, method, path, action, fixedArgs, "", 0)
+               eq(t, "Method", actual.Method, expected.Method)
+               eq(t, "Path", actual.Path, expected.Path)
+               eq(t, "Action", actual.Action, expected.Action)
+               if t.Failed() {
+                       t.Fatal("Failed on route:", routeLine)
+               }
+       }
+}
+
+// Router Tests
+
+const TestRoutes = `
+# This is a comment
+GET            /                   Application.Index
+GET            /test/              Application.Index("Test", "Test2")
+GET            /app/:id/           Application.Show
+GET            /app-wild/*id/          Application.WildShow
+POST           /app/:id            Application.Save
+PATCH          /app/:id/           Application.Update
+PROPFIND       /app/:id                        Application.WebDevMethodPropFind
+MKCOL          /app/:id                        Application.WebDevMethodMkCol
+COPY           /app/:id                        Application.WebDevMethodCopy
+MOVE           /app/:id                        Application.WebDevMethodMove
+PROPPATCH      /app/:id                        Application.WebDevMethodPropPatch
+LOCK           /app/:id                        Application.WebDevMethodLock
+UNLOCK         /app/:id                        Application.WebDevMethodUnLock
+TRACE          /app/:id                        Application.WebDevMethodTrace
+PURGE          /app/:id                        Application.CacheMethodPurge
+GET   /javascript/:filepath      App\Static.Serve("public/js")
+GET   /public/*filepath          Static.Serve("public")
+*     /:controller/:action       :controller.:action
+
+GET   /favicon.ico               404
+`
+
+var routeMatchTestCases = map[*http.Request]*RouteMatch{
+       {
+               Method: "GET",
+               URL:    &url.URL{Path: "/"},
+       }: {
+               ControllerName: "application",
+               MethodName:     "Index",
+               FixedParams:    []string{},
+               Params:         map[string][]string{},
+       },
+
+       {
+               Method: "GET",
+               URL:    &url.URL{Path: "/test/"},
+       }: {
+               ControllerName: "application",
+               MethodName:     "Index",
+               FixedParams:    []string{"Test", "Test2"},
+               Params:         map[string][]string{},
+       },
+
+       {
+               Method: "GET",
+               URL:    &url.URL{Path: "/app/123"},
+       }: {
+               ControllerName: "application",
+               MethodName:     "Show",
+               FixedParams:    []string{},
+               Params:         map[string][]string{"id": {"123"}},
+       },
+
+       {
+               Method: "PATCH",
+               URL:    &url.URL{Path: "/app/123"},
+       }: {
+               ControllerName: "application",
+               MethodName:     "Update",
+               FixedParams:    []string{},
+               Params:         map[string][]string{"id": {"123"}},
+       },
+
+       {
+               Method: "POST",
+               URL:    &url.URL{Path: "/app/123"},
+       }: {
+               ControllerName: "application",
+               MethodName:     "Save",
+               FixedParams:    []string{},
+               Params:         map[string][]string{"id": {"123"}},
+       },
+
+       {
+               Method: "GET",
+               URL:    &url.URL{Path: "/app/123/"},
+       }: {
+               ControllerName: "application",
+               MethodName:     "Show",
+               FixedParams:    []string{},
+               Params:         map[string][]string{"id": {"123"}},
+       },
+
+       {
+               Method: "GET",
+               URL:    &url.URL{Path: "/public/css/style.css"},
+       }: {
+               ControllerName: "static",
+               MethodName:     "Serve",
+               FixedParams:    []string{"public"},
+               Params:         map[string][]string{"filepath": {"css/style.css"}},
+       },
+
+       {
+               Method: "GET",
+               URL:    &url.URL{Path: "/javascript/sessvars.js"},
+       }: {
+               ControllerName: "static",
+               MethodName:     "Serve",
+               FixedParams:    []string{"public/js"},
+               Params:         map[string][]string{"filepath": {"sessvars.js"}},
+       },
+
+       {
+               Method: "GET",
+               URL:    &url.URL{Path: "/Implicit/Route"},
+       }: {
+               ControllerName: "implicit",
+               MethodName:     "Route",
+               FixedParams:    []string{},
+               Params: map[string][]string{
+                       "METHOD":     {"GET"},
+                       "controller": {"Implicit"},
+                       "action":     {"Route"},
+               },
+       },
+
+       {
+               Method: "GET",
+               URL:    &url.URL{Path: "/favicon.ico"},
+       }: {
+               ControllerName: "",
+               MethodName:     "",
+               Action:         "404",
+               FixedParams:    []string{},
+               Params:         map[string][]string{},
+       },
+
+       {
+               Method: "POST",
+               URL:    &url.URL{Path: "/app/123"},
+               Header: http.Header{"X-Http-Method-Override": []string{"PATCH"}},
+       }: {
+               ControllerName: "application",
+               MethodName:     "Update",
+               FixedParams:    []string{},
+               Params:         map[string][]string{"id": {"123"}},
+       },
+
+       {
+               Method: "GET",
+               URL:    &url.URL{Path: "/app/123"},
+               Header: http.Header{"X-Http-Method-Override": []string{"PATCH"}},
+       }: {
+               ControllerName: "application",
+               MethodName:     "Show",
+               FixedParams:    []string{},
+               Params:         map[string][]string{"id": {"123"}},
+       },
+
+       {
+               Method: "PATCH",
+               URL:    &url.URL{Path: "/app/123"},
+       }: {
+               ControllerName: "application",
+               MethodName:     "Update",
+               FixedParams:    []string{},
+               Params:         map[string][]string{"id": {"123"}},
+       },
+
+       {
+               Method: "PROPFIND",
+               URL:    &url.URL{Path: "/app/123"},
+       }: {
+               ControllerName: "application",
+               MethodName:     "WebDevMethodPropFind",
+               FixedParams:    []string{},
+               Params:         map[string][]string{"id": {"123"}},
+       },
+
+       {
+               Method: "MKCOL",
+               URL:    &url.URL{Path: "/app/123"},
+       }: {
+               ControllerName: "application",
+               MethodName:     "WebDevMethodMkCol",
+               FixedParams:    []string{},
+               Params:         map[string][]string{"id": {"123"}},
+       },
+
+       {
+               Method: "COPY",
+               URL:    &url.URL{Path: "/app/123"},
+       }: {
+               ControllerName: "application",
+               MethodName:     "WebDevMethodCopy",
+               FixedParams:    []string{},
+               Params:         map[string][]string{"id": {"123"}},
+       },
+
+       {
+               Method: "MOVE",
+               URL:    &url.URL{Path: "/app/123"},
+       }: {
+               ControllerName: "application",
+               MethodName:     "WebDevMethodMove",
+               FixedParams:    []string{},
+               Params:         map[string][]string{"id": {"123"}},
+       },
+
+       {
+               Method: "PROPPATCH",
+               URL:    &url.URL{Path: "/app/123"},
+       }: {
+               ControllerName: "application",
+               MethodName:     "WebDevMethodPropPatch",
+               FixedParams:    []string{},
+               Params:         map[string][]string{"id": {"123"}},
+       },
+       {
+               Method: "LOCK",
+               URL:    &url.URL{Path: "/app/123"},
+       }: {
+               ControllerName: "application",
+               MethodName:     "WebDevMethodLock",
+               FixedParams:    []string{},
+               Params:         map[string][]string{"id": {"123"}},
+       },
+
+       {
+               Method: "UNLOCK",
+               URL:    &url.URL{Path: "/app/123"},
+       }: {
+               ControllerName: "application",
+               MethodName:     "WebDevMethodUnLock",
+               FixedParams:    []string{},
+               Params:         map[string][]string{"id": {"123"}},
+       },
+
+       {
+               Method: "TRACE",
+               URL:    &url.URL{Path: "/app/123"},
+       }: {
+               ControllerName: "application",
+               MethodName:     "WebDevMethodTrace",
+               FixedParams:    []string{},
+               Params:         map[string][]string{"id": {"123"}},
+       },
+
+       {
+               Method: "PURGE",
+               URL:    &url.URL{Path: "/app/123"},
+       }: {
+               ControllerName: "application",
+               MethodName:     "CacheMethodPurge",
+               FixedParams:    []string{},
+               Params:         map[string][]string{"id": {"123"}},
+       },
+}
+
+func TestRouteMatches(t *testing.T) {
+       initControllers()
+       BasePath = "/BasePath"
+       router := NewRouter("")
+       router.Routes, _ = parseRoutes(appModule, "", "", TestRoutes, false)
+       if err := router.updateTree(); err != nil {
+               t.Errorf("updateTree failed: %s", err)
+       }
+       for req, expected := range routeMatchTestCases {
+               t.Log("Routing:", req.Method, req.URL)
+
+               context := NewGoContext(nil)
+               context.Request.SetRequest(req)
+               c := NewTestController(nil, req)
+
+               actual := router.Route(c.Request)
+               if !eq(t, "Found route", actual != nil, expected != nil) {
+                       continue
+               }
+               if expected.ControllerName != "" {
+                       eq(t, "ControllerName", actual.ControllerName, appModule.Namespace()+expected.ControllerName)
+               } else {
+                       eq(t, "ControllerName", actual.ControllerName, expected.ControllerName)
+               }
+
+               eq(t, "MethodName", actual.MethodName, strings.ToLower(expected.MethodName))
+               eq(t, "len(Params)", len(actual.Params), len(expected.Params))
+               for key, actualValue := range actual.Params {
+                       eq(t, "Params "+key, actualValue[0], expected.Params[key][0])
+               }
+               eq(t, "len(FixedParams)", len(actual.FixedParams), len(expected.FixedParams))
+               for i, actualValue := range actual.FixedParams {
+                       eq(t, "FixedParams", actualValue, expected.FixedParams[i])
+               }
+       }
+}
+
+// Reverse Routing
+
+type ReverseRouteArgs struct {
+       action string
+       args   map[string]string
+}
+
+var reverseRoutingTestCases = map[*ReverseRouteArgs]*ActionDefinition{
+       {
+               action: "Application.Index",
+               args:   map[string]string{},
+       }: {
+               URL:    "/",
+               Method: "GET",
+               Star:   false,
+               Action: "Application.Index",
+       },
+
+       {
+               action: "Application.Show",
+               args:   map[string]string{"id": "123"},
+       }: {
+               URL:    "/app/123/",
+               Method: "GET",
+               Star:   false,
+               Action: "Application.Show",
+       },
+
+       {
+               action: "Implicit.Route",
+               args:   map[string]string{},
+       }: {
+               URL:    "/implicit/route",
+               Method: "GET",
+               Star:   true,
+               Action: "Implicit.Route",
+       },
+
+       {
+               action: "Application.Save",
+               args:   map[string]string{"id": "123", "c": "http://continue"},
+       }: {
+               URL:    "/app/123?c=http%3A%2F%2Fcontinue",
+               Method: "POST",
+               Star:   false,
+               Action: "Application.Save",
+       },
+
+       {
+               action: "Application.WildShow",
+               args:   map[string]string{"id": "123"},
+       }: {
+               URL:    "/app-wild/123/",
+               Method: "GET",
+               Star:   false,
+               Action: "Application.WildShow",
+       },
+
+       {
+               action: "Application.WebDevMethodPropFind",
+               args:   map[string]string{"id": "123"},
+       }: {
+               URL:    "/app/123",
+               Method: "PROPFIND",
+               Star:   false,
+               Action: "Application.WebDevMethodPropFind",
+       },
+       {
+               action: "Application.WebDevMethodMkCol",
+               args:   map[string]string{"id": "123"},
+       }: {
+               URL:    "/app/123",
+               Method: "MKCOL",
+               Star:   false,
+               Action: "Application.WebDevMethodMkCol",
+       },
+       {
+               action: "Application.WebDevMethodCopy",
+               args:   map[string]string{"id": "123"},
+       }: {
+               URL:    "/app/123",
+               Method: "COPY",
+               Star:   false,
+               Action: "Application.WebDevMethodCopy",
+       },
+       {
+               action: "Application.WebDevMethodMove",
+               args:   map[string]string{"id": "123"},
+       }: {
+               URL:    "/app/123",
+               Method: "MOVE",
+               Star:   false,
+               Action: "Application.WebDevMethodMove",
+       },
+       {
+               action: "Application.WebDevMethodPropPatch",
+               args:   map[string]string{"id": "123"},
+       }: {
+               URL:    "/app/123",
+               Method: "PROPPATCH",
+               Star:   false,
+               Action: "Application.WebDevMethodPropPatch",
+       },
+       {
+               action: "Application.WebDevMethodLock",
+               args:   map[string]string{"id": "123"},
+       }: {
+               URL:    "/app/123",
+               Method: "LOCK",
+               Star:   false,
+               Action: "Application.WebDevMethodLock",
+       },
+       {
+               action: "Application.WebDevMethodUnLock",
+               args:   map[string]string{"id": "123"},
+       }: {
+               URL:    "/app/123",
+               Method: "UNLOCK",
+               Star:   false,
+               Action: "Application.WebDevMethodUnLock",
+       },
+       {
+               action: "Application.WebDevMethodTrace",
+               args:   map[string]string{"id": "123"},
+       }: {
+               URL:    "/app/123",
+               Method: "TRACE",
+               Star:   false,
+               Action: "Application.WebDevMethodTrace",
+       },
+       {
+               action: "Application.CacheMethodPurge",
+               args:   map[string]string{"id": "123"},
+       }: {
+               URL:    "/app/123",
+               Method: "PURGE",
+               Star:   false,
+               Action: "Application.CacheMethodPurge",
+       },
+}
+
+type testController struct {
+       *Controller
+}
+
+func initControllers() {
+       registerControllers()
+}
+func TestReverseRouting(t *testing.T) {
+       initControllers()
+       router := NewRouter("")
+       router.Routes, _ = parseRoutes(appModule, "", "", TestRoutes, false)
+       for routeArgs, expected := range reverseRoutingTestCases {
+               actual := router.Reverse(routeArgs.action, routeArgs.args)
+               if !eq(t, fmt.Sprintf("Found route %s %s", routeArgs.action, actual), actual != nil, expected != nil) {
+                       continue
+               }
+               eq(t, "Url", actual.URL, expected.URL)
+               eq(t, "Method", actual.Method, expected.Method)
+               eq(t, "Star", actual.Star, expected.Star)
+               eq(t, "Action", actual.Action, expected.Action)
+       }
+}
+
+func BenchmarkRouter(b *testing.B) {
+       router := NewRouter("")
+       router.Routes, _ = parseRoutes(nil, "", "", TestRoutes, false)
+       if err := router.updateTree(); err != nil {
+               b.Errorf("updateTree failed: %s", err)
+       }
+       b.ResetTimer()
+       for i := 0; i < b.N/len(routeMatchTestCases); i++ {
+               for req := range routeMatchTestCases {
+                       c := NewTestController(nil, req)
+                       r := router.Route(c.Request)
+                       if r == nil {
+                               b.Errorf("Request not found: %s", req.URL.Path)
+                       }
+               }
+       }
+}
+
+// The benchmark from github.com/ant0ine/go-urlrouter
+func BenchmarkLargeRouter(b *testing.B) {
+       router := NewRouter("")
+
+       routePaths := []string{
+               "/",
+               "/signin",
+               "/signout",
+               "/profile",
+               "/settings",
+               "/upload/*file",
+       }
+       for i := 0; i < 10; i++ {
+               for j := 0; j < 5; j++ {
+                       routePaths = append(routePaths, fmt.Sprintf("/resource%d/:id/property%d", i, j))
+               }
+               routePaths = append(routePaths, fmt.Sprintf("/resource%d/:id", i))
+               routePaths = append(routePaths, fmt.Sprintf("/resource%d", i))
+       }
+       routePaths = append(routePaths, "/:any")
+
+       for _, p := range routePaths {
+               router.Routes = append(router.Routes,
+                       NewRoute(appModule, "GET", p, "Controller.Action", "", "", 0))
+       }
+       if err := router.updateTree(); err != nil {
+               b.Errorf("updateTree failed: %s", err)
+       }
+
+       requestUrls := []string{
+               "http://example.org/",
+               "http://example.org/resource9/123",
+               "http://example.org/resource9/123/property1",
+               "http://example.org/doesnotexist",
+       }
+       var reqs []*http.Request
+       for _, url := range requestUrls {
+               req, _ := http.NewRequest("GET", url, nil)
+               reqs = append(reqs, req)
+       }
+
+       b.ResetTimer()
+
+       for i := 0; i < b.N/len(reqs); i++ {
+               for _, req := range reqs {
+                       c := NewTestController(nil, req)
+                       route := router.Route(c.Request)
+                       if route == nil {
+                               b.Errorf("Failed to route: %s", req.URL.Path)
+                       }
+               }
+       }
+}
+
+func BenchmarkRouterFilter(b *testing.B) {
+       startFakeBookingApp()
+       controllers := []*Controller{
+               NewTestController(nil, showRequest),
+               NewTestController(nil, staticRequest),
+       }
+       for _, c := range controllers {
+               c.Params = &Params{}
+               ParseParams(c.Params, c.Request)
+       }
+
+       b.ResetTimer()
+       for i := 0; i < b.N/len(controllers); i++ {
+               for _, c := range controllers {
+                       RouterFilter(c, NilChain)
+               }
+       }
+}
+
+func TestOverrideMethodFilter(t *testing.T) {
+       req, _ := http.NewRequest("POST", "/hotels/3", strings.NewReader("_method=put"))
+       req.Header.Set("Content-Type", "application/x-www-form-urlencoded; param=value")
+       c := NewTestController(nil, req)
+
+       if HTTPMethodOverride(c, NilChain); c.Request.Method != "PUT" {
+               t.Errorf("Expected to override current method '%s' in route, found '%s' instead", "", c.Request.Method)
+       }
+}
+
+// Helpers
+
+func eq(t *testing.T, name string, a, b interface{}) bool {
+       if a != b {
+               t.Error(name, ": (actual)", a, " != ", b, "(expected)")
+               return false
+       }
+       return true
+}
diff --git a/src/foundation/api/revel/server-engine.go b/src/foundation/api/revel/server-engine.go
new file mode 100644 (file)
index 0000000..0ac4b8a
--- /dev/null
@@ -0,0 +1,229 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "errors"
+       "io"
+       "mime/multipart"
+       "net/url"
+       "strings"
+       "time"
+)
+
+const (
+       /* Minimum Engine Type Values */
+       _ = iota
+       ENGINE_RESPONSE_STATUS
+       ENGINE_WRITER
+       ENGINE_PARAMETERS
+       ENGINE_PATH
+       ENGINE_REQUEST
+       ENGINE_RESPONSE
+)
+const (
+       /* HTTP Engine Type Values Starts at 1000 */
+       HTTP_QUERY           = ENGINE_PARAMETERS
+       HTTP_PATH            = ENGINE_PATH
+       HTTP_BODY            = iota + 1000
+       HTTP_FORM            = iota + 1000
+       HTTP_MULTIPART_FORM  = iota + 1000
+       HTTP_METHOD          = iota + 1000
+       HTTP_REQUEST_URI     = iota + 1000
+       HTTP_REQUEST_CONTEXT = iota + 1000
+       HTTP_REMOTE_ADDR     = iota + 1000
+       HTTP_HOST            = iota + 1000
+       HTTP_URL             = iota + 1000
+       HTTP_SERVER_HEADER   = iota + 1000
+       HTTP_STREAM_WRITER   = iota + 1000
+       HTTP_WRITER          = ENGINE_WRITER
+)
+
+type (
+       ServerContext interface {
+               GetRequest() ServerRequest
+               GetResponse() ServerResponse
+       }
+
+       // Callback ServerRequest type
+       ServerRequest interface {
+               GetRaw() interface{}
+               Get(theType int) (interface{}, error)
+               Set(theType int, theValue interface{}) bool
+       }
+       // Callback ServerResponse type
+       ServerResponse interface {
+               ServerRequest
+       }
+       // Callback WebSocket type
+       ServerWebSocket interface {
+               ServerResponse
+               MessageSendJSON(v interface{}) error
+               MessageReceiveJSON(v interface{}) error
+               MessageSend(v interface{}) error
+               MessageReceive(v interface{}) error
+       }
+
+       // Expected response for HTTP_SERVER_HEADER type (if implemented)
+       ServerHeader interface {
+               SetCookie(cookie string) // Sets the cookie
+               GetCookie(key string) (value ServerCookie, err error) // Gets the cookie
+               Set(key string, value string)
+               Add(key string, value string)
+               Del(key string)
+               Get(key string) (value []string)
+               GetKeys() (headerKeys []string)
+               SetStatus(statusCode int)
+       }
+
+       // Expected response for FROM_HTTP_COOKIE type (if implemented)
+       ServerCookie interface {
+               GetValue() string
+       }
+
+       // Expected response for HTTP_MULTIPART_FORM
+       ServerMultipartForm interface {
+               GetFiles() map[string][]*multipart.FileHeader
+               GetValues() url.Values
+               RemoveAll() error
+       }
+       StreamWriter interface {
+               WriteStream(name string, contentlen int64, modtime time.Time, reader io.Reader) error
+       }
+
+       ServerEngine interface {
+               // Initialize the server (non blocking)
+               Init(init *EngineInit)
+               // Starts the server. This will block until server is stopped
+               Start()
+               // Fires a new event to the server
+               Event(event Event, args interface{}) EventResponse
+               // Returns the engine instance for specific calls
+               Engine() interface{}
+               // Returns the engine Name
+               Name() string
+               // Returns any stats
+               Stats() map[string]interface{}
+       }
+
+       // The initialization structure passed into the engine
+       EngineInit struct {
+               Address, // The address
+               Network string // The network
+               Port        int                 // The port
+               HTTPMuxList ServerMuxList       // The HTTPMux
+               Callback    func(ServerContext) // The ServerContext callback endpoint
+       }
+
+       // An empty server engine
+       ServerEngineEmpty struct {
+       }
+
+       // The route handler structure
+       ServerMux struct {
+               PathPrefix string      // The path prefix
+               Callback   interface{} // The callback interface as appropriate to the server
+       }
+
+       // A list of handlers used for adding special route functions
+       ServerMuxList []ServerMux
+)
+
+// Sorting function
+func (r ServerMuxList) Len() int {
+       return len(r)
+}
+
+// Sorting function
+func (r ServerMuxList) Less(i, j int) bool {
+       return len(r[i].PathPrefix) > len(r[j].PathPrefix)
+}
+
+// Sorting function
+func (r ServerMuxList) Swap(i, j int) {
+       r[i], r[j] = r[j], r[i]
+}
+
+// Search function, returns the largest path matching this
+func (r ServerMuxList) Find(path string) (interface{}, bool) {
+       for _, p := range r {
+               if p.PathPrefix == path || strings.HasPrefix(path, p.PathPrefix) {
+                       return p.Callback, true
+               }
+       }
+       return nil, false
+}
+
+// Adds this routehandler to the route table. It will be called (if the path prefix matches)
+// before the Revel mux, this can only be called after the ENGINE_BEFORE_INITIALIZED event
+func AddHTTPMux(path string, callback interface{}) {
+       ServerEngineInit.HTTPMuxList = append(ServerEngineInit.HTTPMuxList, ServerMux{PathPrefix: path, Callback: callback})
+}
+
+// Callback point for the server to handle the
+func handleInternal(ctx ServerContext) {
+       start := time.Now()
+       var c *Controller
+       if RevelConfig.Controller.Reuse {
+               c         = RevelConfig.Controller.Stack.Pop().(*Controller)
+               defer func() {
+                       RevelConfig.Controller.Stack.Push(c)
+               }()
+       } else {
+               c = NewControllerEmpty()
+       }
+
+       var (
+
+               req, resp = c.Request, c.Response
+       )
+       c.SetController(ctx)
+       req.WebSocket, _ = ctx.GetResponse().(ServerWebSocket)
+
+       clientIP := ClientIP(req)
+
+       // Once finished in the internal, we can return these to the stack
+
+       c.ClientIP = clientIP
+       c.Log = AppLog.New("ip", clientIP,
+               "path", req.GetPath(), "method", req.Method)
+       // Call the first filter, this will process the request
+       Filters[0](c, Filters[1:])
+       if c.Result != nil {
+               c.Result.Apply(req, resp)
+       } else if c.Response.Status != 0 {
+               c.Response.SetStatus(c.Response.Status)
+       }
+       // Close the Writer if we can
+       if w, ok := resp.GetWriter().(io.Closer); ok {
+               _ = w.Close()
+       }
+
+       // Revel request access log format
+       // RequestStartTime ClientIP ResponseStatus RequestLatency HTTPMethod URLPath
+       // Sample format: terminal format
+       // INFO 2017/08/02 22:31:41 server-engine.go:168: Request Stats                            ip=::1 path=/public/img/favicon.png method=GET action=Static.Serve namespace=static\\ start=2017/08/02 22:31:41 status=200 duration_seconds=0.0007656
+       // Recommended storing format to json code which looks like
+       // {"action":"Static.Serve","caller":"server-engine.go:168","duration_seconds":0.00058336,"ip":"::1","lvl":3,
+       // "method":"GET","msg":"Request Stats","namespace":"static\\","path":"/public/img/favicon.png",
+       // "start":"2017-08-02T22:34:08-0700","status":200,"t":"2017-08-02T22:34:08.303112145-07:00"}
+
+       c.Log.Info("Request Stats",
+               "start", start,
+               "status", c.Response.Status,
+               "duration_seconds", time.Since(start).Seconds(), "section", "requestlog",
+       )
+}
+
+var (
+       ENGINE_UNKNOWN_GET = errors.New("Server Engine Invalid Get")
+)
+
+func (e *ServerEngineEmpty) Get(_ string) interface{} {
+       return nil
+}
+func (e *ServerEngineEmpty) Set(_ string, _ interface{}) bool {
+       return false
+}
diff --git a/src/foundation/api/revel/server.go b/src/foundation/api/revel/server.go
new file mode 100644 (file)
index 0000000..0f5c786
--- /dev/null
@@ -0,0 +1,153 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "fmt"
+       "github.com/revel/revel/session"
+       "os"
+       "strconv"
+       "strings"
+       "github.com/revel/revel/utils"
+)
+
+// Revel's variables server, router, etc
+var (
+       MainRouter         *Router
+       MainTemplateLoader *TemplateLoader
+       MainWatcher        *Watcher
+       serverEngineMap    = map[string]func() ServerEngine{}
+       CurrentEngine      ServerEngine
+       ServerEngineInit   *EngineInit
+       serverLogger       = RevelLog.New("section", "server")
+)
+
+func RegisterServerEngine(name string, loader func() ServerEngine) {
+       serverLogger.Debug("RegisterServerEngine: Registered engine ", "name", name)
+       serverEngineMap[name] = loader
+}
+
+// InitServer initializes the server and returns the handler
+// It can be used as an alternative entry-point if one needs the http handler
+// to be exposed. E.g. to run on multiple addresses and ports or to set custom
+// TLS options.
+func InitServer() {
+       CurrentEngine.Init(ServerEngineInit)
+       initControllerStack()
+       startupHooks.Run()
+
+       // Load templates
+       MainTemplateLoader = NewTemplateLoader(TemplatePaths)
+       if err := MainTemplateLoader.Refresh(); err != nil {
+               serverLogger.Debug("InitServer: Main template loader failed to refresh", "error", err)
+       }
+
+       // The "watch" config variable can turn on and off all watching.
+       // (As a convenient way to control it all together.)
+       if Config.BoolDefault("watch", true) {
+               MainWatcher = NewWatcher()
+               Filters = append([]Filter{WatchFilter}, Filters...)
+       }
+
+       // If desired (or by default), create a watcher for templates and routes.
+       // The watcher calls Refresh() on things on the first request.
+       if MainWatcher != nil && Config.BoolDefault("watch.templates", true) {
+               MainWatcher.Listen(MainTemplateLoader, MainTemplateLoader.paths...)
+       }
+
+}
+
+// Run the server.
+// This is called from the generated main file.
+// If port is non-zero, use that.  Else, read the port from app.conf.
+func Run(port int) {
+       defer func() {
+               if r := recover(); r != nil {
+                       RevelLog.Crit("Recovered error in startup", "error", r)
+                       RaiseEvent(REVEL_FAILURE, r)
+                       panic("Fatal error in startup")
+               }
+       }()
+
+       // Initialize the session logger, must be initiated from this app to avoid
+       // circular references
+       session.InitSession(RevelLog)
+
+       // Create the CurrentEngine instance from the application config
+       InitServerEngine(port, Config.StringDefault("server.engine", GO_NATIVE_SERVER_ENGINE))
+       RaiseEvent(ENGINE_BEFORE_INITIALIZED, nil)
+       InitServer()
+       RaiseEvent(ENGINE_STARTED, nil)
+       // This is needed for the harness to recognize that the server is started, it looks for the word
+       // "Listening" in the stdout stream
+
+       fmt.Fprintf(os.Stdout, "Revel engine is listening on.. %s\n", ServerEngineInit.Address)
+       // Start never returns,
+       CurrentEngine.Start()
+       fmt.Fprintf(os.Stdout, "Revel engine is NOT listening on.. %s\n", ServerEngineInit.Address)
+       RaiseEvent(ENGINE_SHUTDOWN, nil)
+       shutdownHooks.Run()
+       println("\nRevel exited normally\n")
+}
+
+// Build an engine initialization object and start the engine
+func InitServerEngine(port int, serverEngine string) {
+       address := HTTPAddr
+       if address == "" {
+               address = "localhost"
+       }
+       if port == 0 {
+               port = HTTPPort
+       }
+
+       var network = "tcp"
+       var localAddress string
+
+       // If the port is zero, treat the address as a fully qualified local address.
+       // This address must be prefixed with the network type followed by a colon,
+       // e.g. unix:/tmp/app.socket or tcp6:::1 (equivalent to tcp6:0:0:0:0:0:0:0:1)
+       if port == 0 {
+               parts := strings.SplitN(address, ":", 2)
+               network = parts[0]
+               localAddress = parts[1]
+       } else {
+               localAddress = address + ":" + strconv.Itoa(port)
+       }
+
+       if engineLoader, ok := serverEngineMap[serverEngine]; !ok {
+               panic("Server Engine " + serverEngine + " Not found")
+       } else {
+               CurrentEngine = engineLoader()
+               serverLogger.Debug("InitServerEngine: Found server engine and invoking", "name", CurrentEngine.Name())
+               ServerEngineInit = &EngineInit{
+                       Address:  localAddress,
+                       Network:  network,
+                       Port:     port,
+                       Callback: handleInternal,
+               }
+       }
+       AddInitEventHandler(CurrentEngine.Event)
+}
+
+// Initialize the controller stack for the application
+func initControllerStack() {
+       RevelConfig.Controller.Reuse = Config.BoolDefault("revel.controller.reuse",true)
+
+       if RevelConfig.Controller.Reuse {
+               RevelConfig.Controller.Stack = utils.NewStackLock(
+                       Config.IntDefault("revel.controller.stack", 10),
+                       Config.IntDefault("revel.controller.maxstack", 200), func() interface{} {
+                               return NewControllerEmpty()
+                       })
+               RevelConfig.Controller.CachedStackSize = Config.IntDefault("revel.cache.controller.stack", 10)
+               RevelConfig.Controller.CachedStackMaxSize = Config.IntDefault("revel.cache.controller.maxstack", 100)
+               RevelConfig.Controller.CachedMap = map[string]*utils.SimpleLockStack{}
+       }
+}
+
+// Called to stop the server
+func StopServer(value interface{}) EventResponse {
+       return RaiseEvent(ENGINE_SHUTDOWN_REQUEST,value)
+}
\ No newline at end of file
diff --git a/src/foundation/api/revel/server_adapter_go.go b/src/foundation/api/revel/server_adapter_go.go
new file mode 100644 (file)
index 0000000..bacaf91
--- /dev/null
@@ -0,0 +1,647 @@
+package revel
+
+import (
+       "net"
+       "net/http"
+       "time"
+       "context"
+       "golang.org/x/net/websocket"
+       "io"
+       "mime/multipart"
+       "net/url"
+       "os"
+       "os/signal"
+       "path"
+       "sort"
+       "strconv"
+       "strings"
+       "github.com/revel/revel/utils"
+)
+
+// Register the GoHttpServer engine
+func init() {
+       AddInitEventHandler(func(typeOf Event, value interface{}) (responseOf EventResponse) {
+               if typeOf == REVEL_BEFORE_MODULES_LOADED {
+                       RegisterServerEngine(GO_NATIVE_SERVER_ENGINE, func() ServerEngine { return &GoHttpServer{} })
+               }
+               return
+       })
+}
+
+// The Go HTTP server
+type GoHttpServer struct {
+       Server               *http.Server           // The server instance
+       ServerInit           *EngineInit            // The server engine initialization
+       MaxMultipartSize     int64                  // The largest size of file to accept
+       goContextStack       *utils.SimpleLockStack // The context stack Set via server.context.stack, server.context.maxstack
+       goMultipartFormStack *utils.SimpleLockStack // The multipart form stack set via server.form.stack, server.form.maxstack
+       HttpMuxList          ServerMuxList
+       HasAppMux            bool
+       signalChan           chan os.Signal
+}
+
+// Called to initialize the server with this EngineInit
+func (g *GoHttpServer) Init(init *EngineInit) {
+       g.MaxMultipartSize = int64(Config.IntDefault("server.request.max.multipart.filesize", 32)) << 20 /* 32 MB */
+       g.goContextStack = utils.NewStackLock(Config.IntDefault("server.context.stack", 100),
+               Config.IntDefault("server.context.maxstack", 200),
+               func() interface{} {
+                       return NewGoContext(g)
+               })
+       g.goMultipartFormStack = utils.NewStackLock(Config.IntDefault("server.form.stack", 100),
+               Config.IntDefault("server.form.maxstack", 200),
+               func() interface{} { return &GoMultipartForm{} })
+       g.ServerInit = init
+
+       revelHandler := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
+               g.Handle(writer, request)
+       })
+
+       // Adds the mux list
+       g.HttpMuxList = init.HTTPMuxList
+       sort.Sort(g.HttpMuxList)
+       g.HasAppMux = len(g.HttpMuxList) > 0
+       g.signalChan = make(chan os.Signal)
+
+       g.Server = &http.Server{
+               Addr:         init.Address,
+               Handler:      revelHandler,
+               ReadTimeout:  time.Duration(Config.IntDefault("http.timeout.read", 0)) * time.Second,
+               WriteTimeout: time.Duration(Config.IntDefault("http.timeout.write", 0)) * time.Second,
+       }
+
+}
+
+// Handler is assigned in the Init
+func (g *GoHttpServer) Start() {
+       go func() {
+               time.Sleep(100 * time.Millisecond)
+               serverLogger.Debugf("Start: Listening on %s...", g.Server.Addr)
+       }()
+       if HTTPSsl {
+               if g.ServerInit.Network != "tcp" {
+                       // This limitation is just to reduce complexity, since it is standard
+                       // to terminate SSL upstream when using unix domain sockets.
+                       serverLogger.Fatal("SSL is only supported for TCP sockets. Specify a port to listen on.")
+               }
+               serverLogger.Fatal("Failed to listen:", "error",
+                       g.Server.ListenAndServeTLS(HTTPSslCert, HTTPSslKey))
+       } else {
+               listener, err := net.Listen(g.ServerInit.Network, g.Server.Addr)
+               if err != nil {
+                       serverLogger.Fatal("Failed to listen:", "error", err)
+               }
+               serverLogger.Warn("Server exiting:", "error", g.Server.Serve(listener))
+       }
+}
+
+// Handle the request and response for the server
+func (g *GoHttpServer) Handle(w http.ResponseWriter, r *http.Request) {
+       // This section is called if the developer has added custom mux to the app
+       if g.HasAppMux && g.handleAppMux(w, r) {
+               return
+       }
+       g.handleMux(w, r)
+}
+
+// Handle the request and response for the servers mux
+func (g *GoHttpServer) handleAppMux(w http.ResponseWriter, r *http.Request) bool {
+       // Check the prefix and split them
+       requestPath := path.Clean(r.URL.Path)
+       if handler, hasHandler := g.HttpMuxList.Find(requestPath); hasHandler {
+               clientIP := HttpClientIP(r)
+               localLog := AppLog.New("ip", clientIP,
+                       "path", r.URL.Path, "method", r.Method)
+               defer func() {
+                       if err := recover(); err != nil {
+                               localLog.Error("An error was caught using the handler", "path", requestPath, "error", err)
+                               w.WriteHeader(http.StatusInternalServerError)
+                       }
+               }()
+               start := time.Now()
+               handler.(http.HandlerFunc)(w, r)
+               localLog.Info("Request Stats",
+                       "start", start,
+                       "duration_seconds", time.Since(start).Seconds(), "section", "requestlog",
+               )
+               return true
+       }
+       return false
+}
+
+// Passes the server request to Revel
+func (g *GoHttpServer) handleMux(w http.ResponseWriter, r *http.Request) {
+       if maxRequestSize := int64(Config.IntDefault("http.maxrequestsize", 0)); maxRequestSize > 0 {
+               r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
+       }
+
+       upgrade := r.Header.Get("Upgrade")
+       context := g.goContextStack.Pop().(*GoContext)
+       defer func() {
+               g.goContextStack.Push(context)
+       }()
+       context.Request.SetRequest(r)
+       context.Response.SetResponse(w)
+
+       if upgrade == "websocket" || upgrade == "Websocket" {
+               websocket.Handler(func(ws *websocket.Conn) {
+                       //Override default Read/Write timeout with sane value for a web socket request
+                       if err := ws.SetDeadline(time.Now().Add(time.Hour * 24)); err != nil {
+                               serverLogger.Error("SetDeadLine failed:", err)
+                       }
+                       r.Method = "WS"
+                       context.Request.WebSocket = ws
+                       context.WebSocket = &GoWebSocket{Conn: ws, GoResponse: *context.Response}
+                       g.ServerInit.Callback(context)
+               }).ServeHTTP(w, r)
+       } else {
+               g.ServerInit.Callback(context)
+       }
+}
+
+// ClientIP method returns client IP address from HTTP request.
+//
+// Note: Set property "app.behind.proxy" to true only if Revel is running
+// behind proxy like nginx, haproxy, apache, etc. Otherwise
+// you may get inaccurate Client IP address. Revel parses the
+// IP address in the order of X-Forwarded-For, X-Real-IP.
+//
+// By default revel will get http.Request's RemoteAddr
+func HttpClientIP(r *http.Request) string {
+       if Config.BoolDefault("app.behind.proxy", false) {
+               // Header X-Forwarded-For
+               if fwdFor := strings.TrimSpace(r.Header.Get(HdrForwardedFor)); fwdFor != "" {
+                       index := strings.Index(fwdFor, ",")
+                       if index == -1 {
+                               return fwdFor
+                       }
+                       return fwdFor[:index]
+               }
+
+               // Header X-Real-Ip
+               if realIP := strings.TrimSpace(r.Header.Get(HdrRealIP)); realIP != "" {
+                       return realIP
+               }
+       }
+
+       if remoteAddr, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
+               return remoteAddr
+       }
+
+       return ""
+}
+
+// The server key name
+const GO_NATIVE_SERVER_ENGINE = "go"
+
+// Returns the name of this engine
+func (g *GoHttpServer) Name() string {
+       return GO_NATIVE_SERVER_ENGINE
+}
+
+// Returns stats for this engine
+func (g *GoHttpServer) Stats() map[string]interface{} {
+       return map[string]interface{}{
+               "Go Engine Context": g.goContextStack.String(),
+               "Go Engine Forms":   g.goMultipartFormStack.String(),
+       }
+}
+
+// Return the engine instance
+func (g *GoHttpServer) Engine() interface{} {
+       return g.Server
+}
+
+// Handles an event from Revel
+func (g *GoHttpServer) Event(event Event, args interface{}) (r EventResponse) {
+       switch event {
+       case ENGINE_STARTED:
+               signal.Notify(g.signalChan, os.Interrupt, os.Kill)
+               go func() {
+                       _ = <-g.signalChan
+                       serverLogger.Info("Received quit singal Please wait ... ")
+                       RaiseEvent(ENGINE_SHUTDOWN_REQUEST, nil)
+               }()
+       case ENGINE_SHUTDOWN_REQUEST:
+               ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(Config.IntDefault("app.cancel.timeout", 60)))
+               defer cancel()
+               g.Server.Shutdown(ctx)
+       default:
+
+       }
+
+       return
+}
+
+type (
+       // The go context
+       GoContext struct {
+               Request   *GoRequest   // The request
+               Response  *GoResponse  // The response
+               WebSocket *GoWebSocket // The websocket
+       }
+
+       // The go request
+       GoRequest struct {
+               Original        *http.Request    // The original
+               FormParsed      bool             // True if form parsed
+               MultiFormParsed bool             // True if multipart form parsed
+               WebSocket       *websocket.Conn  // The websocket
+               ParsedForm      *GoMultipartForm // The parsed form data
+               Goheader        *GoHeader        // The header
+               Engine          *GoHttpServer    // THe server
+       }
+
+       // The response
+       GoResponse struct {
+               Original http.ResponseWriter // The original writer
+               Goheader *GoHeader           // The header
+               Writer   io.Writer           // The writer
+               Request  *GoRequest          // The request
+               Engine   *GoHttpServer       // The engine
+       }
+
+       // The multipart form
+       GoMultipartForm struct {
+               Form *multipart.Form // The form
+       }
+
+       // The go header
+       GoHeader struct {
+               Source     interface{} // The source
+               isResponse bool        // True if response header
+       }
+
+       // The websocket
+       GoWebSocket struct {
+               Conn       *websocket.Conn // The connection
+               GoResponse                 // The response
+       }
+
+       // The cookie
+       GoCookie http.Cookie
+)
+
+// Create a new go context
+func NewGoContext(instance *GoHttpServer) *GoContext {
+       // This bit in here is for the test cases, which pass in a nil value
+       if instance == nil {
+               instance = &GoHttpServer{MaxMultipartSize: 32 << 20}
+               instance.goContextStack = utils.NewStackLock(100, 200,
+                       func() interface{} {
+                               return NewGoContext(instance)
+                       })
+               instance.goMultipartFormStack = utils.NewStackLock(100, 200,
+                       func() interface{} { return &GoMultipartForm{} })
+       }
+       c := &GoContext{Request: &GoRequest{Goheader: &GoHeader{}, Engine: instance}}
+       c.Response = &GoResponse{Goheader: &GoHeader{}, Request: c.Request, Engine: instance}
+       return c
+}
+
+// get the request
+func (c *GoContext) GetRequest() ServerRequest {
+       return c.Request
+}
+
+// Get the response
+func (c *GoContext) GetResponse() ServerResponse {
+       if c.WebSocket != nil {
+               return c.WebSocket
+       }
+       return c.Response
+}
+
+// Destroy the context
+func (c *GoContext) Destroy() {
+       c.Response.Destroy()
+       c.Request.Destroy()
+       if c.WebSocket != nil {
+               c.WebSocket.Destroy()
+               c.WebSocket = nil
+       }
+}
+
+// Communicate with the server engine to exchange data
+func (r *GoRequest) Get(key int) (value interface{}, err error) {
+       switch key {
+       case HTTP_SERVER_HEADER:
+               value = r.GetHeader()
+       case HTTP_MULTIPART_FORM:
+               value, err = r.GetMultipartForm()
+       case HTTP_QUERY:
+               value = r.Original.URL.Query()
+       case HTTP_FORM:
+               value, err = r.GetForm()
+       case HTTP_REQUEST_URI:
+               value = r.Original.URL.String()
+       case HTTP_REQUEST_CONTEXT:
+               value = r.Original.Context()
+       case HTTP_REMOTE_ADDR:
+               value = r.Original.RemoteAddr
+       case HTTP_METHOD:
+               value = r.Original.Method
+       case HTTP_PATH:
+               value = r.Original.URL.Path
+       case HTTP_HOST:
+               value = r.Original.Host
+       case HTTP_URL:
+               value = r.Original.URL
+       case HTTP_BODY:
+               value = r.Original.Body
+       default:
+               err = ENGINE_UNKNOWN_GET
+       }
+
+       return
+}
+
+// Sets the request key with value
+func (r *GoRequest) Set(key int, value interface{}) bool {
+       return false
+}
+
+// Returns the form
+func (r *GoRequest) GetForm() (url.Values, error) {
+       if !r.FormParsed {
+               if e := r.Original.ParseForm(); e != nil {
+                       return nil, e
+               }
+               r.FormParsed = true
+       }
+
+       return r.Original.Form, nil
+}
+
+// Returns the form
+func (r *GoRequest) GetMultipartForm() (ServerMultipartForm, error) {
+       if !r.MultiFormParsed {
+               if e := r.Original.ParseMultipartForm(r.Engine.MaxMultipartSize); e != nil {
+                       return nil, e
+               }
+               r.ParsedForm = r.Engine.goMultipartFormStack.Pop().(*GoMultipartForm)
+               r.ParsedForm.Form = r.Original.MultipartForm
+       }
+
+       return r.ParsedForm, nil
+}
+
+// Returns the header
+func (r *GoRequest) GetHeader() ServerHeader {
+       return r.Goheader
+}
+
+// Returns the raw value
+func (r *GoRequest) GetRaw() interface{} {
+       return r.Original
+}
+
+// Sets the request
+func (r *GoRequest) SetRequest(req *http.Request) {
+       r.Original = req
+       r.Goheader.Source = r
+       r.Goheader.isResponse = false
+
+}
+
+// Destroy the request
+func (r *GoRequest) Destroy() {
+       r.Goheader.Source = nil
+       r.Original = nil
+       r.FormParsed = false
+       r.MultiFormParsed = false
+       r.ParsedForm = nil
+}
+
+// Gets the key from the response
+func (r *GoResponse) Get(key int) (value interface{}, err error) {
+       switch key {
+       case HTTP_SERVER_HEADER:
+               value = r.Header()
+       case HTTP_STREAM_WRITER:
+               value = r
+       case HTTP_WRITER:
+               value = r.Writer
+       default:
+               err = ENGINE_UNKNOWN_GET
+       }
+       return
+}
+
+// Sets the key with the value
+func (r *GoResponse) Set(key int, value interface{}) (set bool) {
+       switch key {
+       case ENGINE_RESPONSE_STATUS:
+               r.Header().SetStatus(value.(int))
+               set = true
+       case HTTP_WRITER:
+               r.SetWriter(value.(io.Writer))
+               set = true
+       }
+       return
+}
+
+// Sets the header
+func (r *GoResponse) Header() ServerHeader {
+       return r.Goheader
+}
+
+// Gets the original response
+func (r *GoResponse) GetRaw() interface{} {
+       return r.Original
+}
+
+// Sets the writer
+func (r *GoResponse) SetWriter(writer io.Writer) {
+       r.Writer = writer
+}
+
+// Write output to stream
+func (r *GoResponse) WriteStream(name string, contentlen int64, modtime time.Time, reader io.Reader) error {
+       // Check to see if the output stream is modified, if not send it using the
+       // Native writer
+       written := false
+       if _, ok := r.Writer.(http.ResponseWriter); ok {
+               if rs, ok := reader.(io.ReadSeeker); ok {
+                       http.ServeContent(r.Original, r.Request.Original, name, modtime, rs)
+                       written = true
+               }
+       }
+       if !written {
+               // Else, do a simple io.Copy.
+               ius := r.Request.Original.Header.Get("If-Unmodified-Since")
+               if t, err := http.ParseTime(ius); err == nil && !modtime.IsZero() {
+                       // The Date-Modified header truncates sub-second precision, so
+                       // use mtime < t+1s instead of mtime <= t to check for unmodified.
+                       if modtime.Before(t.Add(1 * time.Second)) {
+                               h := r.Original.Header()
+                               delete(h, "Content-Type")
+                               delete(h, "Content-Length")
+                               if h.Get("Etag") != "" {
+                                       delete(h, "Last-Modified")
+                               }
+                               r.Original.WriteHeader(http.StatusNotModified)
+                               return nil
+                       }
+               }
+
+               if contentlen != -1 {
+                       header := ServerHeader(r.Goheader)
+                       if writer, found := r.Writer.(*CompressResponseWriter); found {
+                               header = ServerHeader(writer.Header)
+                       }
+                       header.Set("Content-Length", strconv.FormatInt(contentlen, 10))
+               }
+               if _, err := io.Copy(r.Writer, reader); err != nil {
+                       r.Original.WriteHeader(http.StatusInternalServerError)
+                       return err
+               } else {
+                       r.Original.WriteHeader(http.StatusOK)
+               }
+       }
+       return nil
+}
+
+// Frees response
+func (r *GoResponse) Destroy() {
+       if c, ok := r.Writer.(io.Closer); ok {
+               c.Close()
+       }
+       r.Goheader.Source = nil
+       r.Original = nil
+       r.Writer = nil
+}
+
+// Sets the response
+func (r *GoResponse) SetResponse(w http.ResponseWriter) {
+       r.Original = w
+       r.Writer = w
+       r.Goheader.Source = r
+       r.Goheader.isResponse = true
+
+}
+
+// Sets the cookie
+func (r *GoHeader) SetCookie(cookie string) {
+       if r.isResponse {
+               r.Source.(*GoResponse).Original.Header().Add("Set-Cookie", cookie)
+       }
+}
+
+// Gets the cookie
+func (r *GoHeader) GetCookie(key string) (value ServerCookie, err error) {
+       if !r.isResponse {
+               var cookie *http.Cookie
+               if cookie, err = r.Source.(*GoRequest).Original.Cookie(key); err == nil {
+                       value = GoCookie(*cookie)
+
+               }
+
+       }
+       return
+}
+
+// Sets (replaces) header key
+func (r *GoHeader) Set(key string, value string) {
+       if r.isResponse {
+               r.Source.(*GoResponse).Original.Header().Set(key, value)
+       }
+}
+
+// Adds the header key
+func (r *GoHeader) Add(key string, value string) {
+       if r.isResponse {
+               r.Source.(*GoResponse).Original.Header().Add(key, value)
+       }
+}
+
+// Deletes the header key
+func (r *GoHeader) Del(key string) {
+       if r.isResponse {
+               r.Source.(*GoResponse).Original.Header().Del(key)
+       }
+}
+
+// Gets the header key
+func (r *GoHeader) Get(key string) (value []string) {
+       if !r.isResponse {
+               value = r.Source.(*GoRequest).Original.Header[key]
+               if len(value) == 0 {
+                       if ihead := r.Source.(*GoRequest).Original.Header.Get(key); ihead != "" {
+                               value = append(value, ihead)
+                       }
+               }
+       } else {
+               value = r.Source.(*GoResponse).Original.Header()[key]
+       }
+       return
+}
+
+// Returns list of header keys
+func (r *GoHeader) GetKeys() (value []string) {
+       if !r.isResponse {
+               for key := range r.Source.(*GoRequest).Original.Header {
+                       value = append(value, key)
+               }
+       } else {
+               for key := range r.Source.(*GoResponse).Original.Header() {
+                       value = append(value, key)
+               }
+       }
+       return
+}
+
+// Sets the status of the header
+func (r *GoHeader) SetStatus(statusCode int) {
+       if r.isResponse {
+               r.Source.(*GoResponse).Original.WriteHeader(statusCode)
+       }
+}
+
+// Return cookies value
+func (r GoCookie) GetValue() string {
+       return r.Value
+}
+
+// Return files from the form
+func (f *GoMultipartForm) GetFiles() map[string][]*multipart.FileHeader {
+       return f.Form.File
+}
+
+// Return values from the form
+func (f *GoMultipartForm) GetValues() url.Values {
+       return url.Values(f.Form.Value)
+}
+
+// Remove all values from the form freeing memory
+func (f *GoMultipartForm) RemoveAll() error {
+       return f.Form.RemoveAll()
+}
+
+/**
+ * Message send JSON
+ */
+func (g *GoWebSocket) MessageSendJSON(v interface{}) error {
+       return websocket.JSON.Send(g.Conn, v)
+}
+
+/**
+ * Message receive JSON
+ */
+func (g *GoWebSocket) MessageReceiveJSON(v interface{}) error {
+       return websocket.JSON.Receive(g.Conn, v)
+}
+
+/**
+ * Message Send
+ */
+func (g *GoWebSocket) MessageSend(v interface{}) error {
+       return websocket.Message.Send(g.Conn, v)
+}
+
+/**
+ * Message receive
+ */
+func (g *GoWebSocket) MessageReceive(v interface{}) error {
+       return websocket.Message.Receive(g.Conn, v)
+}
diff --git a/src/foundation/api/revel/server_test.go b/src/foundation/api/revel/server_test.go
new file mode 100644 (file)
index 0000000..e37fa24
--- /dev/null
@@ -0,0 +1,148 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "github.com/stretchr/testify/assert"
+       "net/http"
+       "net/http/httptest"
+       "os"
+       "path/filepath"
+       "strings"
+       "testing"
+       "time"
+)
+
+// This tries to benchmark the usual request-serving pipeline to get an overall
+// performance metric.
+//
+// Each iteration runs one mock request to display a hotel's detail page by id.
+//
+// Contributing parts:
+// - Routing
+// - Controller lookup / invocation
+// - Parameter binding
+// - Session, flash, i18n cookies
+// - Render() call magic
+// - Template rendering
+func BenchmarkServeAction(b *testing.B) {
+       benchmarkRequest(b, showRequest)
+}
+
+func BenchmarkServeJson(b *testing.B) {
+       benchmarkRequest(b, jsonRequest)
+}
+
+func BenchmarkServePlaintext(b *testing.B) {
+       benchmarkRequest(b, plaintextRequest)
+}
+
+// This tries to benchmark the static serving overhead when serving an "average
+// size" 7k file.
+func BenchmarkServeStatic(b *testing.B) {
+       benchmarkRequest(b, staticRequest)
+}
+
+func benchmarkRequest(b *testing.B, req *http.Request) {
+       startFakeBookingApp()
+       b.ResetTimer()
+       resp := httptest.NewRecorder()
+       for i := 0; i < b.N; i++ {
+               CurrentEngine.(*GoHttpServer).Handle(resp, req)
+       }
+}
+
+// Test that the booking app can be successfully run for a test.
+func TestFakeServer(t *testing.T) {
+       startFakeBookingApp()
+
+       resp := httptest.NewRecorder()
+
+       // First, test that the expected responses are actually generated
+       CurrentEngine.(*GoHttpServer).Handle(resp, showRequest)
+       if !strings.Contains(resp.Body.String(), "300 Main St.") {
+               t.Errorf("Failed to find hotel address in action response:\n%s", resp.Body)
+               t.FailNow()
+       }
+       resp.Body.Reset()
+
+       CurrentEngine.(*GoHttpServer).Handle(resp, staticRequest)
+       sessvarsSize := getFileSize(t, filepath.Join(BasePath, "public", "js", "sessvars.js"))
+       if int64(resp.Body.Len()) != sessvarsSize {
+               t.Errorf("Expected sessvars.js to have %d bytes, got %d:\n%s", sessvarsSize, resp.Body.Len(), resp.Body)
+               t.FailNow()
+       }
+       resp.Body.Reset()
+
+       CurrentEngine.(*GoHttpServer).Handle(resp, jsonRequest)
+       if !strings.Contains(resp.Body.String(), `"Address":"300 Main St."`) {
+               t.Errorf("Failed to find hotel address in JSON response:\n%s", resp.Body)
+               t.FailNow()
+       }
+       resp.Body.Reset()
+
+       CurrentEngine.(*GoHttpServer).Handle(resp, plaintextRequest)
+       if resp.Body.String() != "Hello, World!" {
+               t.Errorf("Failed to find greeting in plaintext response:\n%s", resp.Body)
+               t.FailNow()
+       }
+
+       resp.Body = nil
+}
+
+func getFileSize(t *testing.T, name string) int64 {
+       fi, err := os.Stat(name)
+       if err != nil {
+               t.Errorf("Unable to stat file:\n%s", name)
+               t.FailNow()
+       }
+       return fi.Size()
+}
+
+// Ensure on app start runs in order
+func TestOnAppStart(t *testing.T) {
+       str := ""
+       a := assert.New(t)
+       OnAppStart(func() {
+               str += " World"
+       }, 2)
+
+       OnAppStart(func() {
+               str += "Hello"
+       }, 1)
+
+       startFakeBookingApp()
+
+       a.Equal("Hello World", str, "Failed to order OnAppStart")
+}
+
+// Ensure on app stop runs in order
+func TestOnAppStop(t *testing.T) {
+       a := assert.New(t)
+       startFakeBookingApp()
+       i := ""
+       OnAppStop(func() {
+               i += "cruel world"
+               t.Logf("i: %v \n", i)
+       }, 2)
+       OnAppStop(func() {
+               i += "goodbye "
+               t.Logf("i: %v \n", i)
+       }, 1)
+       go func() {
+               time.Sleep(2 * time.Second)
+               RaiseEvent(ENGINE_SHUTDOWN_REQUEST, nil)
+       }()
+       Run(0)
+       a.Equal("goodbye cruel world", i, "Did not get shutdown events")
+
+}
+
+var (
+       showRequest, _      = http.NewRequest("GET", "/hotels/3", nil)
+       staticRequest, _    = http.NewRequest("GET", "/public/js/sessvars.js", nil)
+       jsonRequest, _      = http.NewRequest("GET", "/hotels/3/booking", nil)
+       plaintextRequest, _ = http.NewRequest("GET", "/hotels", nil)
+)
diff --git a/src/foundation/api/revel/session/init.go b/src/foundation/api/revel/session/init.go
new file mode 100644 (file)
index 0000000..56069e5
--- /dev/null
@@ -0,0 +1,10 @@
+package session
+
+// The logger for the session
+import "github.com/revel/revel/logger"
+
+var sessionLog logger.MultiLogger
+
+func InitSession(coreLogger logger.MultiLogger) {
+       sessionLog = coreLogger.New("section", "session")
+}
diff --git a/src/foundation/api/revel/session/session.go b/src/foundation/api/revel/session/session.go
new file mode 100644 (file)
index 0000000..c5696bc
--- /dev/null
@@ -0,0 +1,364 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package session
+
+import (
+       "encoding/hex"
+       "encoding/json"
+       "errors"
+       "github.com/twinj/uuid"
+       "reflect"
+       "strconv"
+       "strings"
+       "time"
+)
+
+const (
+       // The key for the identity of the session
+       SessionIDKey = "_ID"
+       // The expiration date of the session
+       TimestampKey = "_TS"
+       // The value name indicating how long the session should persist - ie should it persist after the browser closes
+       // this is set under the TimestampKey if the session data should expire immediately
+       SessionValueName = "session"
+       // The key container for the json objects of the data, any non strings found in the map will be placed in here
+       // serialized by key using JSON
+       SessionObjectKeyName = "_object_"
+       // The mapped session object
+       SessionMapKeyName = "_map_"
+       // The suffix of the session cookie
+       SessionCookieSuffix = "_SESSION"
+)
+
+// Session data, can be any data, there are reserved keywords used by the storage data
+// SessionIDKey Is the key name for the session
+// TimestampKey Is the time that the session should expire
+//
+type Session map[string]interface{}
+
+func NewSession() Session {
+       return Session{}
+}
+
+// ID retrieves from the cookie or creates a time-based UUID identifying this
+// session.
+func (s Session) ID() string {
+       if sessionIDStr, ok := s[SessionIDKey]; ok {
+               return sessionIDStr.(string)
+       }
+
+       buffer := uuid.NewV4()
+
+       s[SessionIDKey] = hex.EncodeToString(buffer.Bytes())
+       return s[SessionIDKey].(string)
+}
+
+// getExpiration return a time.Time with the session's expiration date.
+// It uses the passed in expireAfterDuration to add with the current time if the timeout is not
+// browser dependent (ie session). If previous session has set to "session", the time returned is time.IsZero()
+func (s Session) GetExpiration(expireAfterDuration time.Duration) time.Time {
+       if expireAfterDuration == 0 || s[TimestampKey] == SessionValueName {
+               // Expire after closing browser
+               return time.Time{}
+       }
+       return time.Now().Add(expireAfterDuration)
+}
+
+// SetNoExpiration sets session to expire when browser session ends
+func (s Session) SetNoExpiration() {
+       s[TimestampKey] = SessionValueName
+}
+
+// SetDefaultExpiration sets session to expire after default duration
+func (s Session) SetDefaultExpiration() {
+       delete(s, TimestampKey)
+}
+
+// sessionTimeoutExpiredOrMissing returns a boolean of whether the session
+// cookie is either not present or present but beyond its time to live; i.e.,
+// whether there is not a valid session.
+func (s Session) SessionTimeoutExpiredOrMissing() bool {
+       if exp, present := s[TimestampKey]; !present {
+               return true
+       } else if exp == SessionValueName {
+               return false
+       } else if expInt, _ := strconv.Atoi(exp.(string)); int64(expInt) < time.Now().Unix() {
+               return true
+       }
+       return false
+}
+
+// Constant error if session value is not found
+var SESSION_VALUE_NOT_FOUND = errors.New("Session value not found")
+
+// Get an object or property from the session
+// it may be embedded inside the session.
+func (s Session) Get(key string) (newValue interface{}, err error) {
+       // First check to see if it is in the session
+       if v, found := s[key]; found {
+               return v, nil
+       }
+       return s.GetInto(key, nil, false)
+}
+
+// Get into the specified value.
+// If value exists in the session it will just return the value
+func (s Session) GetInto(key string, target interface{}, force bool) (result interface{}, err error) {
+       if v, found := s[key]; found && !force {
+               return v, nil
+       }
+       splitKey := strings.Split(key, ".")
+       rootKey := splitKey[0]
+
+       // Force always recreates the object from the session data map
+       if force {
+               if target == nil {
+                       if result, err = s.sessionDataFromMap(key); err != nil {
+                               return
+                       }
+               } else if result, err = s.sessionDataFromObject(rootKey, target); err != nil {
+                       return
+               }
+
+               return s.getNestedProperty(splitKey, result)
+       }
+
+       // Attempt to find the key in the session, this is the most generalized form
+       v, found := s[rootKey]
+       if !found {
+               if target == nil {
+                       // Try to fetch it from the session
+
+                       if v, err = s.sessionDataFromMap(rootKey); err != nil {
+                               return
+                       }
+               } else if v, err = s.sessionDataFromObject(rootKey, target); err != nil {
+                       return
+               }
+       }
+
+       return s.getNestedProperty(splitKey, v)
+}
+
+// Returns the default value if the key is not found
+func (s Session) GetDefault(key string, value interface{}, defaultValue interface{}) interface{} {
+       v, e := s.GetInto(key, value, false)
+       if e != nil {
+               v = defaultValue
+       }
+       return v
+}
+
+// Extract the values from the session
+func (s Session) GetProperty(key string, value interface{}) (interface{}, error) {
+       // Capitalize the first letter
+       key = strings.Title(key)
+
+       sessionLog.Info("getProperty", "key", key, "value", value)
+
+       // For a map it is easy
+       if reflect.TypeOf(value).Kind() == reflect.Map {
+               val := reflect.ValueOf(value)
+               valueOf := val.MapIndex(reflect.ValueOf(key))
+               if valueOf == reflect.Zero(reflect.ValueOf(value).Type()) {
+                       return nil, nil
+               }
+               //idx := val.MapIndex(reflect.ValueOf(key))
+               if !valueOf.IsValid() {
+                       return nil, nil
+               }
+
+               return valueOf.Interface(), nil
+       }
+
+       objValue := s.reflectValue(value)
+       field := objValue.FieldByName(key)
+       if !field.IsValid() {
+               return nil, SESSION_VALUE_NOT_FOUND
+       }
+
+       return field.Interface(), nil
+}
+
+// Places the object into the session, a nil value will cause remove the key from the session
+// (or you can use the Session.Del(key) function
+func (s Session) Set(key string, value interface{}) error {
+       if value == nil {
+               s.Del(key)
+               return nil
+       }
+
+       s[key] = value
+       return nil
+}
+
+// Delete the key from the sessionObjects and Session
+func (s Session) Del(key string) {
+       sessionJsonMap := s.getSessionJsonMap()
+       delete(sessionJsonMap, key)
+       delete(s, key)
+}
+
+// Extracts the session as a map of [string keys] and json values
+func (s Session) getSessionJsonMap() map[string]string {
+       if sessionJson, found := s[SessionObjectKeyName]; found {
+               if _, valid := sessionJson.(map[string]string); !valid {
+                       sessionLog.Error("Session object key corrupted, reset", "was", sessionJson)
+                       s[SessionObjectKeyName] = map[string]string{}
+               }
+               // serialized data inside the session _objects
+       } else {
+               s[SessionObjectKeyName] = map[string]string{}
+       }
+
+       return s[SessionObjectKeyName].(map[string]string)
+}
+
+// Convert the map to a simple map[string]string map
+// this will marshal any non string objects encountered and store them the the jsonMap
+// The expiration time will also be assigned
+func (s Session) Serialize() map[string]string {
+       sessionJsonMap := s.getSessionJsonMap()
+       newMap := map[string]string{}
+       newObjectMap := map[string]string{}
+       for key, value := range sessionJsonMap {
+               newObjectMap[key] = value
+       }
+       for key, value := range s {
+               if key == SessionObjectKeyName || key == SessionMapKeyName {
+                       continue
+               }
+               if reflect.ValueOf(value).Kind() == reflect.String {
+                       newMap[key] = value.(string)
+                       continue
+               }
+               println("Serialize the data for", key)
+               if data, err := json.Marshal(value); err != nil {
+                       sessionLog.Error("Unable to marshal session ", "key", key, "error", err)
+                       continue
+               } else {
+                       newObjectMap[key] = string(data)
+               }
+       }
+       if len(newObjectMap) > 0 {
+               if data, err := json.Marshal(newObjectMap); err != nil {
+                       sessionLog.Error("Unable to marshal session ", "key", SessionObjectKeyName, "error", err)
+
+               } else {
+                       newMap[SessionObjectKeyName] = string(data)
+               }
+       }
+
+       return newMap
+}
+
+// Set the session object from the loaded data
+func (s Session) Load(data map[string]string) {
+       for key, value := range data {
+               if key == SessionObjectKeyName {
+                       target := map[string]string{}
+                       if err := json.Unmarshal([]byte(value), &target); err != nil {
+                               sessionLog.Error("Unable to unmarshal session ", "key", SessionObjectKeyName, "error", err)
+                       } else {
+                               s[key] = target
+                       }
+               } else {
+                       s[key] = value
+               }
+
+       }
+}
+
+// Checks to see if the session is empty
+func (s Session) Empty() bool {
+       i := 0
+       for k := range s {
+               i++
+               if k == SessionObjectKeyName || k == SessionMapKeyName {
+                       continue
+               }
+       }
+       return i == 0
+}
+
+func (s *Session) reflectValue(obj interface{}) reflect.Value {
+       var val reflect.Value
+
+       if reflect.TypeOf(obj).Kind() == reflect.Ptr {
+               val = reflect.ValueOf(obj).Elem()
+       } else {
+               val = reflect.ValueOf(obj)
+       }
+
+       return val
+}
+
+// Starting at position 1 drill into the object
+func (s Session) getNestedProperty(keys []string, newValue interface{}) (result interface{}, err error) {
+       for x := 1; x < len(keys); x++ {
+               newValue, err = s.GetProperty(keys[x], newValue)
+               if err != nil || newValue == nil {
+                       return newValue, err
+               }
+       }
+       return newValue, nil
+}
+
+// Always converts the data from the session mapped objects into the target,
+// it will store the results under the session key name SessionMapKeyName
+func (s Session) sessionDataFromMap(key string) (result interface{}, err error) {
+       var mapValue map[string]interface{}
+       uncastMapValue, found := s[SessionMapKeyName]
+       if !found {
+               mapValue = map[string]interface{}{}
+               s[SessionMapKeyName] = mapValue
+       } else if mapValue, found = uncastMapValue.(map[string]interface{}); !found {
+               // Unusual means that the value in the session was not expected
+               sessionLog.Errorf("Unusual means that the value in the session was not expected", "session", uncastMapValue)
+               mapValue = map[string]interface{}{}
+               s[SessionMapKeyName] = mapValue
+       }
+
+       // Try to extract the key from the map
+       result, found = mapValue[key]
+       if !found {
+               result, err = s.convertSessionData(key, nil)
+               if err == nil {
+                       mapValue[key] = result
+               }
+       }
+       return
+}
+
+// Unpack the object from the session map and store it in the session when done, if no error occurs
+func (s Session) sessionDataFromObject(key string, newValue interface{}) (result interface{}, err error) {
+       result, err = s.convertSessionData(key, newValue)
+       if err != nil {
+               return
+       }
+       s[key] = result
+       return
+}
+
+// Converts from the session json map into the target,
+func (s Session) convertSessionData(key string, target interface{}) (result interface{}, err error) {
+       sessionJsonMap := s.getSessionJsonMap()
+       v, found := sessionJsonMap[key]
+       if !found {
+               return target, SESSION_VALUE_NOT_FOUND
+       }
+
+       // Create a target if needed
+       if target == nil {
+               target = map[string]interface{}{}
+               if err := json.Unmarshal([]byte(v), &target); err != nil {
+                       return target, err
+               }
+       } else if err := json.Unmarshal([]byte(v), target); err != nil {
+               return target, err
+       }
+       result = target
+       return
+}
diff --git a/src/foundation/api/revel/session/session_cookie_test.go b/src/foundation/api/revel/session/session_cookie_test.go
new file mode 100644 (file)
index 0000000..ca6c829
--- /dev/null
@@ -0,0 +1,76 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package session_test
+
+import (
+       "testing"
+
+       "github.com/revel/revel"
+       "github.com/revel/revel/session"
+       "github.com/stretchr/testify/assert"
+       "net/http"
+       "time"
+)
+
+func TestCookieRestore(t *testing.T) {
+       a := assert.New(t)
+       session.InitSession(revel.RevelLog)
+
+       cse := revel.NewSessionCookieEngine()
+       originSession := session.NewSession()
+       setSharedDataTest(originSession)
+       originSession["foo"] = "foo"
+       originSession["bar"] = "bar"
+       cookie := cse.GetCookie(originSession)
+       if !cookie.Expires.IsZero() {
+               t.Error("incorrect cookie expire", cookie.Expires)
+       }
+
+       restoredSession := session.NewSession()
+       cse.DecodeCookie(revel.GoCookie(*cookie), restoredSession)
+       a.Equal("foo",restoredSession["foo"])
+       a.Equal("bar",restoredSession["bar"])
+       testSharedData(originSession, restoredSession, t, a)
+}
+
+func TestCookieSessionExpire(t *testing.T) {
+       session.InitSession(revel.RevelLog)
+       cse := revel.NewSessionCookieEngine()
+       cse.ExpireAfterDuration = time.Hour
+       session := session.NewSession()
+       session["user"] = "Tom"
+       var cookie *http.Cookie
+       for i := 0; i < 3; i++ {
+               cookie = cse.GetCookie(session)
+               time.Sleep(time.Second)
+
+               cse.DecodeCookie(revel.GoCookie(*cookie), session)
+       }
+       expectExpire := time.Now().Add(cse.ExpireAfterDuration)
+       if cookie.Expires.Unix() < expectExpire.Add(-time.Second).Unix() {
+               t.Error("expect expires", cookie.Expires, "after", expectExpire.Add(-time.Second))
+       }
+       if cookie.Expires.Unix() > expectExpire.Unix() {
+               t.Error("expect expires", cookie.Expires, "before", expectExpire)
+       }
+
+       // Test that the expiration time is zero for a "browser" session
+       session.SetNoExpiration()
+       cookie = cse.GetCookie(session)
+       if !cookie.Expires.IsZero() {
+               t.Error("expect cookie expires is zero")
+       }
+
+       // Check the default session is set
+       session.SetDefaultExpiration()
+       cookie = cse.GetCookie(session)
+       expectExpire = time.Now().Add(cse.ExpireAfterDuration)
+       if cookie.Expires.Unix() < expectExpire.Add(-time.Second).Unix() {
+               t.Error("expect expires", cookie.Expires, "after", expectExpire.Add(-time.Second))
+       }
+       if cookie.Expires.Unix() > expectExpire.Unix() {
+               t.Error("expect expires", cookie.Expires, "before", expectExpire)
+       }
+}
diff --git a/src/foundation/api/revel/session/session_test.go b/src/foundation/api/revel/session/session_test.go
new file mode 100644 (file)
index 0000000..6be1ce5
--- /dev/null
@@ -0,0 +1,64 @@
+// Copyright (c) 2012-2018 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package session_test
+
+import (
+       "fmt"
+       "github.com/revel/revel"
+       "github.com/revel/revel/session"
+       "github.com/stretchr/testify/assert"
+       "testing"
+)
+
+// test the commands
+func TestSessionString(t *testing.T) {
+       session.InitSession(revel.RevelLog)
+       a := assert.New(t)
+       s := session.NewSession()
+       s.Set("happy", "day")
+       a.Equal("day", s.GetDefault("happy", nil, ""), fmt.Sprintf("Session Data %#v\n", s))
+
+}
+
+func TestSessionStruct(t *testing.T) {
+       session.InitSession(revel.RevelLog)
+       a := assert.New(t)
+       s := session.NewSession()
+       setSharedDataTest(s)
+       a.Equal("test", s.GetDefault("happy.a.aa", nil, ""), fmt.Sprintf("Session Data %#v\n", s))
+
+       stringMap := s.Serialize()
+       s1 := session.NewSession()
+       s1.Load(stringMap)
+       testSharedData(s, s1, t, a)
+
+}
+
+func setSharedDataTest(s session.Session) {
+       data := struct {
+               A struct {
+                       Aa string
+               }
+               B int
+               C string
+               D float32
+       }{A: struct {
+               Aa string
+       }{Aa: "test"},
+               B: 5,
+               C: "test",
+               D: -325.25}
+       s.Set("happy", data)
+}
+func testSharedData(s, s1 session.Session, t *testing.T, a *assert.Assertions) {
+       // Compress the session to a string
+       t.Logf("Original session %#v\n", s)
+       t.Logf("New built session %#v\n", s1)
+       data,err := s1.Get("happy.a.aa")
+       a.Nil(err,"Expected nil")
+       a.Equal("test", data, fmt.Sprintf("Session Data %#v\n", s))
+       t.Logf("After test session %#v\n", s1)
+
+}
diff --git a/src/foundation/api/revel/session_adapter_cookie.go b/src/foundation/api/revel/session_adapter_cookie.go
new file mode 100644 (file)
index 0000000..ebe871d
--- /dev/null
@@ -0,0 +1,144 @@
+package revel
+
+import (
+       "fmt"
+       "github.com/revel/revel/session"
+       "net/http"
+       "net/url"
+       "strconv"
+       "strings"
+       "time"
+)
+
+
+type (
+       // The session cookie engine
+       SessionCookieEngine struct {
+               ExpireAfterDuration time.Duration
+       }
+)
+
+// A logger for the session engine
+var sessionEngineLog = RevelLog.New("section", "session-engine")
+
+// Create a new instance to test
+func init() {
+       RegisterSessionEngine(initCookieEngine, "revel-cookie")
+}
+
+// For testing purposes this engine is used
+func NewSessionCookieEngine() *SessionCookieEngine {
+       ce := &SessionCookieEngine{}
+       return ce
+}
+
+// Called when the the application starts, retrieves data from the app config so cannot be run until then
+func initCookieEngine() SessionEngine {
+       ce := &SessionCookieEngine{}
+
+       var err error
+       if expiresString, ok := Config.String("session.expires"); !ok {
+               ce.ExpireAfterDuration = 30 * 24 * time.Hour
+       } else if expiresString == session.SessionValueName {
+               ce.ExpireAfterDuration = 0
+       } else if ce.ExpireAfterDuration, err = time.ParseDuration(expiresString); err != nil {
+               panic(fmt.Errorf("session.expires invalid: %s", err))
+       }
+
+       return ce
+}
+
+// Decode the session information from the cookie retrieved from the controller request
+func (cse *SessionCookieEngine) Decode(c *Controller) {
+       // Decode the session from a cookie.
+       c.Session = map[string]interface{}{}
+       sessionMap := c.Session
+       if cookie, err := c.Request.Cookie(CookiePrefix + session.SessionCookieSuffix); err != nil {
+               return
+       } else {
+               cse.DecodeCookie(cookie, sessionMap)
+               c.Session = sessionMap
+       }
+}
+
+// Encode the session information to the cookie, set the cookie on the controller
+func (cse *SessionCookieEngine) Encode(c *Controller) {
+
+       c.SetCookie(cse.GetCookie(c.Session))
+}
+
+// Exposed only for testing purposes
+func (cse *SessionCookieEngine) DecodeCookie(cookie ServerCookie, s session.Session) {
+       // Decode the session from a cookie.
+       // Separate the data from the signature.
+       cookieValue := cookie.GetValue()
+       hyphen := strings.Index(cookieValue, "-")
+       if hyphen == -1 || hyphen >= len(cookieValue)-1 {
+               return
+       }
+       sig, data := cookieValue[:hyphen], cookieValue[hyphen+1:]
+
+       // Verify the signature.
+       if !Verify(data, sig) {
+               sessionEngineLog.Warn("Session cookie signature failed")
+               return
+       }
+
+       // Parse the cookie into a temp map, and then load it into the session object
+       tempMap := map[string]string{}
+       ParseKeyValueCookie(data, func(key, val string) {
+               tempMap[key] = val
+       })
+       s.Load(tempMap)
+
+       // Check timeout after unpacking values - if timeout missing (or removed) destroy all session
+       // objects
+       if s.SessionTimeoutExpiredOrMissing() {
+               // If this fails we need to delete all the keys from the session
+               for key := range s {
+                       delete(s, key)
+               }
+       }
+}
+
+// Convert session to cookie
+func (cse *SessionCookieEngine) GetCookie(s session.Session) *http.Cookie {
+       var sessionValue string
+       ts := s.GetExpiration(cse.ExpireAfterDuration)
+       if ts.IsZero() {
+               s[session.TimestampKey] = session.SessionValueName
+       } else {
+               s[session.TimestampKey] = strconv.FormatInt(ts.Unix(), 10)
+       }
+
+       // Convert the key to a string map
+       stringMap := s.Serialize()
+
+       for key, value := range stringMap {
+               if strings.ContainsAny(key, ":\x00") {
+                       panic("Session keys may not have colons or null bytes")
+               }
+               if strings.Contains(value, "\x00") {
+                       panic("Session values may not have null bytes")
+               }
+               sessionValue += "\x00" + key + ":" + value + "\x00"
+       }
+
+       if len(sessionValue) > 1024*4 {
+               sessionEngineLog.Error("SessionCookieEngine.Cookie, session data has exceeded 4k limit (%d) cookie data will not be reliable", "length", len(sessionValue))
+       }
+
+       sessionData := url.QueryEscape(sessionValue)
+       sessionCookie := &http.Cookie{
+               Name:     CookiePrefix + session.SessionCookieSuffix,
+               Value:    Sign(sessionData) + "-" + sessionData,
+               Domain:   CookieDomain,
+               Path:     "/",
+               HttpOnly: true,
+               Secure:   CookieSecure,
+               Expires:  ts.UTC(),
+               MaxAge:   int(cse.ExpireAfterDuration.Seconds()),
+       }
+       return sessionCookie
+
+}
diff --git a/src/foundation/api/revel/session_engine.go b/src/foundation/api/revel/session_engine.go
new file mode 100644 (file)
index 0000000..1f95d7b
--- /dev/null
@@ -0,0 +1,35 @@
+package revel
+
+// The session engine provides an interface to allow for storage of session data
+type (
+       SessionEngine interface {
+               Decode(c *Controller) // Called to decode the session information on the controller
+               Encode(c *Controller) // Called to encode the session information on the controller
+       }
+)
+
+var (
+       sessionEngineMap     = map[string]func() SessionEngine{}
+       CurrentSessionEngine SessionEngine
+)
+
+// Initialize session engine on startup
+func init() {
+       OnAppStart(initSessionEngine, 5)
+}
+
+func RegisterSessionEngine(f func() SessionEngine, name string) {
+       sessionEngineMap[name] = f
+}
+
+// Called when application is starting up
+func initSessionEngine() {
+       // Check for session engine to use and assign it
+       sename := Config.StringDefault("session.engine", "revel-cookie")
+       if se, found := sessionEngineMap[sename]; found {
+               CurrentSessionEngine = se()
+       } else {
+               sessionLog.Warn("Session engine '%s' not found, using default session engine revel-cookie", sename)
+               CurrentSessionEngine = sessionEngineMap["revel-cookie"]()
+       }
+}
diff --git a/src/foundation/api/revel/session_filter.go b/src/foundation/api/revel/session_filter.go
new file mode 100644 (file)
index 0000000..f10470a
--- /dev/null
@@ -0,0 +1,25 @@
+package revel
+
+// SessionFilter is a Revel Filter that retrieves and sets the session cookie.
+// Within Revel, it is available as a Session attribute on Controller instances.
+// The name of the Session cookie is set as CookiePrefix + "_SESSION".
+import ()
+
+var sessionLog = RevelLog.New("section", "session")
+
+func SessionFilter(c *Controller, fc []Filter) {
+       CurrentSessionEngine.Decode(c)
+       sessionWasEmpty := c.Session.Empty()
+
+       // Make session vars available in templates as {{.session.xyz}}
+       c.ViewArgs["session"] = c.Session
+       c.ViewArgs["_controller"] = c
+
+       fc[0](c, fc[1:])
+
+       // If session is not empty or if session was not empty then
+       // pass it back to the session engine to be encoded
+       if !c.Session.Empty() || !sessionWasEmpty {
+               CurrentSessionEngine.Encode(c)
+       }
+}
diff --git a/src/foundation/api/revel/template.go b/src/foundation/api/revel/template.go
new file mode 100644 (file)
index 0000000..c5bd91e
--- /dev/null
@@ -0,0 +1,477 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "bufio"
+       "bytes"
+       "fmt"
+       "io"
+       "io/ioutil"
+       "os"
+       "path/filepath"
+       "regexp"
+       "strconv"
+       "strings"
+       "sync"
+       "sync/atomic"
+)
+
+// ErrorCSSClass httml CSS error class name
+var ErrorCSSClass = "hasError"
+
+// TemplateLoader object handles loading and parsing of templates.
+// Everything below the application's views directory is treated as a template.
+type TemplateLoader struct {
+       // Paths to search for templates, in priority order.
+       paths []string
+       // load version seed for templates
+       loadVersionSeed int
+       // A templateRuntime of looked up template results
+       runtimeLoader atomic.Value
+       // Lock to prevent concurrent map writes
+       templateMutex sync.Mutex
+}
+
+type Template interface {
+       // The name of the template.
+       Name() string // Name of template
+       // The content of the template as a string (Used in error handling).
+       Content() []string // Content
+       // Called by the server to render the template out the io.Writer, context contains the view args to be passed to the template.
+       Render(wr io.Writer, context interface{}) error
+       // The full path to the file on the disk.
+       Location() string // Disk location
+}
+
+var invalidSlugPattern = regexp.MustCompile(`[^a-z0-9 _-]`)
+var whiteSpacePattern = regexp.MustCompile(`\s+`)
+var templateLog = RevelLog.New("section", "template")
+
+// TemplateOutputArgs returns the result of the template rendered using the passed in arguments.
+func TemplateOutputArgs(templatePath string, args map[string]interface{}) (data []byte, err error) {
+       // Get the Template.
+       lang, _ := args[CurrentLocaleViewArg].(string)
+       template, err := MainTemplateLoader.TemplateLang(templatePath, lang)
+       if err != nil {
+               return nil, err
+       }
+       tr := &RenderTemplateResult{
+               Template: template,
+               ViewArgs: args,
+       }
+       b, err := tr.ToBytes()
+       if err != nil {
+               return nil, err
+       }
+       return b.Bytes(), nil
+}
+
+func NewTemplateLoader(paths []string) *TemplateLoader {
+       loader := &TemplateLoader{
+               paths: paths,
+       }
+       return loader
+}
+
+// WatchDir returns true of directory doesn't start with . (dot)
+// otherwise false
+func (loader *TemplateLoader) WatchDir(info os.FileInfo) bool {
+       // Watch all directories, except the ones starting with a dot.
+       return !strings.HasPrefix(info.Name(), ".")
+}
+
+// WatchFile returns true of file doesn't start with . (dot)
+// otherwise false
+func (loader *TemplateLoader) WatchFile(basename string) bool {
+       // Watch all files, except the ones starting with a dot.
+       return !strings.HasPrefix(basename, ".")
+}
+
+// DEPRECATED Use TemplateLang, will be removed in future release
+func (loader *TemplateLoader) Template(name string) (tmpl Template, err error) {
+       runtimeLoader := loader.runtimeLoader.Load().(*templateRuntime)
+       return runtimeLoader.TemplateLang(name, "")
+}
+
+func (loader *TemplateLoader) TemplateLang(name, lang string) (tmpl Template, err error) {
+       runtimeLoader := loader.runtimeLoader.Load().(*templateRuntime)
+       return runtimeLoader.TemplateLang(name, lang)
+}
+
+// Refresh method scans the views directory and parses all templates as Go Templates.
+// If a template fails to parse, the error is set on the loader.
+// (It's awkward to refresh a single Go Template)
+func (loader *TemplateLoader) Refresh() (err *Error) {
+       loader.templateMutex.Lock()
+       defer loader.templateMutex.Unlock()
+
+       loader.loadVersionSeed++
+       runtimeLoader := &templateRuntime{loader: loader,
+               version:     loader.loadVersionSeed,
+               templateMap: map[string]Template{}}
+
+       templateLog.Debug("Refresh: Refreshing templates from ", "path", loader.paths)
+       if err = loader.initializeEngines(runtimeLoader, Config.StringDefault("template.engines", GO_TEMPLATE)); err != nil {
+               return
+       }
+       for _, engine := range runtimeLoader.templatesAndEngineList {
+               engine.Event(TEMPLATE_REFRESH_REQUESTED, nil)
+       }
+       RaiseEvent(TEMPLATE_REFRESH_REQUESTED, nil)
+       defer func() {
+               for _, engine := range runtimeLoader.templatesAndEngineList {
+                       engine.Event(TEMPLATE_REFRESH_COMPLETED, nil)
+               }
+               RaiseEvent(TEMPLATE_REFRESH_COMPLETED, nil)
+
+               // Reset the runtimeLoader
+               loader.runtimeLoader.Store(runtimeLoader)
+       }()
+
+       // Resort the paths, make sure the revel path is the last path,
+       // so anything can override it
+       revelTemplatePath := filepath.Join(RevelPath, "templates")
+       // Go through the paths
+       for i, o := range loader.paths {
+               if o == revelTemplatePath && i != len(loader.paths)-1 {
+                       loader.paths[i] = loader.paths[len(loader.paths)-1]
+                       loader.paths[len(loader.paths)-1] = revelTemplatePath
+               }
+       }
+       templateLog.Debug("Refresh: Refreshing templates from", "path", loader.paths)
+
+       runtimeLoader.compileError = nil
+       runtimeLoader.TemplatePaths = map[string]string{}
+
+       for _, basePath := range loader.paths {
+               // Walk only returns an error if the template loader is completely unusable
+               // (namely, if one of the TemplateFuncs does not have an acceptable signature).
+
+               // Handling symlinked directories
+               var fullSrcDir string
+               f, err := os.Lstat(basePath)
+               if err == nil && f.Mode()&os.ModeSymlink == os.ModeSymlink {
+                       fullSrcDir, err = filepath.EvalSymlinks(basePath)
+                       if err != nil {
+                               templateLog.Panic("Refresh: Eval symlinks error ", "error", err)
+                       }
+               } else {
+                       fullSrcDir = basePath
+               }
+
+               var templateWalker filepath.WalkFunc
+
+               templateWalker = func(path string, info os.FileInfo, err error) error {
+                       if err != nil {
+                               templateLog.Error("Refresh: error walking templates:", "error", err)
+                               return nil
+                       }
+
+                       // Walk into watchable directories
+                       if info.IsDir() {
+                               if !loader.WatchDir(info) {
+                                       return filepath.SkipDir
+                               }
+                               return nil
+                       }
+
+                       // Only add watchable
+                       if !loader.WatchFile(info.Name()) {
+                               return nil
+                       }
+
+                       fileBytes, err := runtimeLoader.findAndAddTemplate(path, fullSrcDir, basePath)
+                       if err != nil {
+                               // Add in this template name to the list of templates unable to be compiled
+                               runtimeLoader.compileErrorNameList = append(runtimeLoader.compileErrorNameList, filepath.ToSlash(path[len(fullSrcDir)+1:]))
+                       }
+                       // Store / report the first error encountered.
+                       if err != nil && runtimeLoader.compileError == nil {
+                               runtimeLoader.compileError, _ = err.(*Error)
+
+                               if nil == runtimeLoader.compileError {
+                                       _, line, description := ParseTemplateError(err)
+
+                                       runtimeLoader.compileError = &Error{
+                                               Title:       "Template Compilation Error",
+                                               Path:        path,
+                                               Description: description,
+                                               Line:        line,
+                                               SourceLines: strings.Split(string(fileBytes), "\n"),
+                                       }
+                               }
+                               templateLog.Errorf("Refresh: Template compilation error (In %s around line %d):\n\t%s",
+                                       path, runtimeLoader.compileError.Line, err.Error())
+                       } else if nil != err { //&& strings.HasPrefix(templateName, "errors/") {
+
+                               if compileError, ok := err.(*Error); ok {
+                                       templateLog.Errorf("Template compilation error (In %s around line %d):\n\t%s",
+                                               path, compileError.Line, err.Error())
+                               } else {
+                                       templateLog.Errorf("Template compilation error (In %s ):\n\t%s",
+                                               path, err.Error())
+                               }
+                       }
+                       return nil
+               }
+
+               if _, err = os.Lstat(fullSrcDir); os.IsNotExist(err) {
+                       // #1058 Given views/template path is not exists
+                       // so no need to walk, move on to next path
+                       continue
+               }
+
+               funcErr := Walk(fullSrcDir, templateWalker)
+
+               // If there was an error with the Funcs, set it and return immediately.
+               if funcErr != nil {
+                       runtimeLoader.compileError = NewErrorFromPanic(funcErr)
+                       return runtimeLoader.compileError
+               }
+       }
+
+       // Note: compileError may or may not be set.
+       return runtimeLoader.compileError
+}
+
+type templateRuntime struct {
+       loader *TemplateLoader
+       // load version for templates
+       version int
+       // Template data and implementation
+       templatesAndEngineList []TemplateEngine
+       // If an error was encountered parsing the templates, it is stored here.
+       compileError *Error
+       // A list of the names of the templates with errors
+       compileErrorNameList []string
+       // Map from template name to the path from whence it was loaded.
+       TemplatePaths map[string]string
+       // A map of looked up template results
+       templateMap map[string]Template
+}
+
+// Checks to see if template exists in templatePaths, if so it is skipped (templates are imported in order
+// reads the template file into memory, replaces namespace keys with module (if found
+func (runtimeLoader *templateRuntime) findAndAddTemplate(path, fullSrcDir, basePath string) (fileBytes []byte, err error) {
+       templateName := filepath.ToSlash(path[len(fullSrcDir)+1:])
+       // Convert template names to use forward slashes, even on Windows.
+       if os.PathSeparator == '\\' {
+               templateName = strings.Replace(templateName, `\`, `/`, -1) // `
+       }
+
+       // Check to see if template was found
+       if place, found := runtimeLoader.TemplatePaths[templateName]; found {
+               templateLog.Debug("findAndAddTemplate: Not Loading, template is already exists: ", "name", templateName, "old",
+                       place, "new", path)
+               return
+       }
+
+       fileBytes, err = ioutil.ReadFile(path)
+       if err != nil {
+               templateLog.Error("findAndAddTemplate: Failed reading file:", "path", path, "error", err)
+               return
+       }
+       // Parse template file and replace the "_LOCAL_|" in the template with the module name
+       // allow for namespaces to be renamed "_LOCAL_(.*?)|"
+       if module := ModuleFromPath(path, false); module != nil {
+               fileBytes = namespaceReplace(fileBytes, module)
+       }
+
+       // if we have an engine picked for this template process it now
+       baseTemplate := NewBaseTemplate(templateName, path, basePath, fileBytes)
+
+       // Try to find a default engine for the file
+       for _, engine := range runtimeLoader.templatesAndEngineList {
+               if engine.Handles(baseTemplate) {
+                       _, err = runtimeLoader.loadIntoEngine(engine, baseTemplate)
+                       return
+               }
+       }
+
+       // Try all engines available
+       var defaultError error
+       for _, engine := range runtimeLoader.templatesAndEngineList {
+               if loaded, loaderr := runtimeLoader.loadIntoEngine(engine, baseTemplate); loaded {
+                       return
+               } else {
+                       templateLog.Debugf("findAndAddTemplate: Engine '%s' unable to compile %s %s", engine.Name(), path, loaderr.Error())
+                       if defaultError == nil {
+                               defaultError = loaderr
+                       }
+               }
+       }
+
+       // Assign the error from the first parser
+       err = defaultError
+
+       // No engines could be found return the err
+       if err == nil {
+               err = fmt.Errorf("Failed to parse template file using engines %s", path)
+       }
+
+       return
+}
+
+func (runtimeLoader *templateRuntime) loadIntoEngine(engine TemplateEngine, baseTemplate *TemplateView) (loaded bool, err error) {
+       if loadedTemplate, found := runtimeLoader.templateMap[baseTemplate.TemplateName]; found {
+               // Duplicate template found in map
+               templateLog.Debug("template already exists in map: ", baseTemplate.TemplateName, " in engine ", engine.Name(), "\r\n\told file:",
+                       loadedTemplate.Location(), "\r\n\tnew file:", baseTemplate.FilePath)
+               return
+       }
+
+       if loadedTemplate := engine.Lookup(baseTemplate.TemplateName); loadedTemplate != nil {
+               // Duplicate template found for engine
+               templateLog.Debug("loadIntoEngine: template already exists: ", "template", baseTemplate.TemplateName, "inengine ", engine.Name(), "old",
+                       loadedTemplate.Location(), "new", baseTemplate.FilePath)
+               loaded = true
+               return
+       }
+       if err = engine.ParseAndAdd(baseTemplate); err == nil {
+               if tmpl := engine.Lookup(baseTemplate.TemplateName); tmpl != nil {
+                       runtimeLoader.templateMap[baseTemplate.TemplateName] = tmpl
+               }
+               runtimeLoader.TemplatePaths[baseTemplate.TemplateName] = baseTemplate.FilePath
+               templateLog.Debugf("loadIntoEngine:Engine '%s' compiled %s", engine.Name(), baseTemplate.FilePath)
+               loaded = true
+       } else {
+               templateLog.Debug("loadIntoEngine: Engine failed to compile", "engine", engine.Name(), "file", baseTemplate.FilePath, "error", err)
+       }
+       return
+}
+
+// Parse the line, and description from an error message like:
+// html/template:Application/Register.html:36: no such template "footer.html"
+func ParseTemplateError(err error) (templateName string, line int, description string) {
+       if e, ok := err.(*Error); ok {
+               return "", e.Line, e.Description
+       }
+
+       description = err.Error()
+       i := regexp.MustCompile(`:\d+:`).FindStringIndex(description)
+       if i != nil {
+               line, err = strconv.Atoi(description[i[0]+1 : i[1]-1])
+               if err != nil {
+                       templateLog.Error("ParseTemplateError: Failed to parse line number from error message:", "error", err)
+               }
+               templateName = description[:i[0]]
+               if colon := strings.Index(templateName, ":"); colon != -1 {
+                       templateName = templateName[colon+1:]
+               }
+               templateName = strings.TrimSpace(templateName)
+               description = description[i[1]+1:]
+       }
+       return templateName, line, description
+}
+
+// Template returns the Template with the given name.  The name is the template's path
+// relative to a template loader root.
+//
+// An Error is returned if there was any problem with any of the templates.  (In
+// this case, if a template is returned, it may still be usable.)
+func (runtimeLoader *templateRuntime) TemplateLang(name, lang string) (tmpl Template, err error) {
+       if runtimeLoader.compileError != nil {
+               for _, errName := range runtimeLoader.compileErrorNameList {
+                       if name == errName {
+                               return nil, runtimeLoader.compileError
+                       }
+               }
+       }
+
+       // Fetch the template from the map
+       tmpl = runtimeLoader.templateLoad(name, lang)
+       if tmpl == nil {
+               err = fmt.Errorf("Template %s not found.", name)
+       }
+
+       return
+}
+
+// Load and also updates map if name is not found (to speed up next lookup)
+func (runtimeLoader *templateRuntime) templateLoad(name, lang string) (tmpl Template) {
+       langName := name
+       found := false
+       if lang != "" {
+               // Look up and return the template.
+               langName = name + "." + lang
+               tmpl, found = runtimeLoader.templateMap[langName]
+               if found {
+                       return
+               }
+               tmpl, found = runtimeLoader.templateMap[name]
+       } else {
+               tmpl, found = runtimeLoader.templateMap[name]
+               if found {
+                       return
+               }
+       }
+
+       if !found {
+               // Neither name is found
+               // Look up and return the template.
+               for _, engine := range runtimeLoader.templatesAndEngineList {
+                       if tmpl = engine.Lookup(langName); tmpl != nil {
+                               found = true
+                               break
+                       }
+                       if tmpl = engine.Lookup(name); tmpl != nil {
+                               found = true
+                               break
+                       }
+               }
+               if !found {
+                       return
+               }
+       }
+
+       // If we found anything store it in the map, we need to copy so we do not
+       // run into concurrency issues
+       runtimeLoader.loader.templateMutex.Lock()
+       defer runtimeLoader.loader.templateMutex.Unlock()
+
+       // In case another thread has loaded the map, reload the atomic value and check
+       newRuntimeLoader := runtimeLoader.loader.runtimeLoader.Load().(*templateRuntime)
+       if newRuntimeLoader.version != runtimeLoader.version {
+               return
+       }
+
+       newTemplateMap := map[string]Template{}
+       for k, v := range newRuntimeLoader.templateMap {
+               newTemplateMap[k] = v
+       }
+       newTemplateMap[langName] = tmpl
+       if _, found := newTemplateMap[name]; !found {
+               newTemplateMap[name] = tmpl
+       }
+       runtimeCopy := &templateRuntime{}
+       *runtimeCopy = *newRuntimeLoader
+       runtimeCopy.templateMap = newTemplateMap
+
+       // Set the atomic value
+       runtimeLoader.loader.runtimeLoader.Store(runtimeCopy)
+       return
+}
+
+func (i *TemplateView) Location() string {
+       return i.FilePath
+}
+
+func (i *TemplateView) Content() (content []string) {
+       if i.FileBytes != nil {
+               // Parse the bytes
+               buffer := bytes.NewBuffer(i.FileBytes)
+               reader := bufio.NewScanner(buffer)
+               for reader.Scan() {
+                       content = append(content, string(reader.Bytes()))
+               }
+       }
+
+       return content
+}
+
+func NewBaseTemplate(templateName, filePath, basePath string, fileBytes []byte) *TemplateView {
+       return &TemplateView{TemplateName: templateName, FilePath: filePath, FileBytes: fileBytes, BasePath: basePath}
+}
diff --git a/src/foundation/api/revel/template_adapter_go.go b/src/foundation/api/revel/template_adapter_go.go
new file mode 100644 (file)
index 0000000..4111307
--- /dev/null
@@ -0,0 +1,129 @@
+package revel
+
+import (
+       "html/template"
+       "io"
+       "log"
+       "strings"
+)
+
+const GO_TEMPLATE = "go"
+
+// Called on startup, initialized when the REVEL_BEFORE_MODULES_LOADED is called
+func init() {
+       AddInitEventHandler(func(typeOf Event, value interface{}) (responseOf EventResponse) {
+               if typeOf == REVEL_BEFORE_MODULES_LOADED {
+                       RegisterTemplateLoader(GO_TEMPLATE, func(loader *TemplateLoader) (TemplateEngine, error) {
+                               // Set the template delimiters for the project if present, then split into left
+                               // and right delimiters around a space character
+
+                               TemplateDelims := Config.StringDefault("template.go.delimiters", "")
+                               var splitDelims []string
+                               if TemplateDelims != "" {
+                                       splitDelims = strings.Split(TemplateDelims, " ")
+                                       if len(splitDelims) != 2 {
+                                               log.Fatalln("app.conf: Incorrect format for template.delimiters")
+                                       }
+                               }
+
+                               return &GoEngine{
+                                       loader:          loader,
+                                       templateSet:     template.New("__root__").Funcs(TemplateFuncs),
+                                       templatesByName: map[string]*GoTemplate{},
+                                       splitDelims:     splitDelims,
+                               }, nil
+                       })
+               }
+               return
+       })
+}
+
+// Adapter for Go Templates.
+type GoTemplate struct {
+       *template.Template
+       engine *GoEngine
+       *TemplateView
+}
+
+// return a 'revel.Template' from Go's template.
+func (gotmpl GoTemplate) Render(wr io.Writer, arg interface{}) error {
+       return gotmpl.Execute(wr, arg)
+}
+
+// The main template engine for Go
+type GoEngine struct {
+       // The template loader
+       loader *TemplateLoader
+       // THe current template set
+       templateSet *template.Template
+       // A map of templates by name
+       templatesByName map[string]*GoTemplate
+       // The delimiter that is used to indicate template code, defaults to {{
+       splitDelims []string
+       // True if map is case insensitive
+       CaseInsensitive bool
+}
+
+// Convert the path to lower case if needed
+func (i *GoEngine) ConvertPath(path string) string {
+       if i.CaseInsensitive {
+               return strings.ToLower(path)
+       }
+       return path
+}
+
+// Returns true if this engine can handle the response
+func (i *GoEngine) Handles(templateView *TemplateView) bool {
+       return EngineHandles(i, templateView)
+}
+
+// Parses the template vide and adds it to the template set
+func (engine *GoEngine) ParseAndAdd(baseTemplate *TemplateView) error {
+       // If alternate delimiters set for the project, change them for this set
+       if engine.splitDelims != nil && strings.Index(baseTemplate.Location(), ViewsPath) > -1 {
+               engine.templateSet.Delims(engine.splitDelims[0], engine.splitDelims[1])
+       } else {
+               // Reset to default otherwise
+               engine.templateSet.Delims("", "")
+       }
+       templateSource := string(baseTemplate.FileBytes)
+       templateName := engine.ConvertPath(baseTemplate.TemplateName)
+       tpl, err := engine.templateSet.New(baseTemplate.TemplateName).Parse(templateSource)
+       if nil != err {
+               _, line, description := ParseTemplateError(err)
+               return &Error{
+                       Title:       "Template Compilation Error",
+                       Path:        baseTemplate.TemplateName,
+                       Description: description,
+                       Line:        line,
+                       SourceLines: strings.Split(templateSource, "\n"),
+               }
+       }
+       engine.templatesByName[templateName] = &GoTemplate{Template: tpl, engine: engine, TemplateView: baseTemplate}
+       return nil
+}
+
+// Lookups the template name, to see if it is contained in this engine
+func (engine *GoEngine) Lookup(templateName string) Template {
+       // Case-insensitive matching of template file name
+       if tpl, found := engine.templatesByName[engine.ConvertPath(templateName)]; found {
+               return tpl
+       }
+       return nil
+}
+
+// Return the engine name
+func (engine *GoEngine) Name() string {
+       return GO_TEMPLATE
+}
+
+// An event listener to listen for Revel INIT events
+func (engine *GoEngine) Event(action Event, i interface{}) {
+       if action == TEMPLATE_REFRESH_REQUESTED {
+               // At this point all the templates have been passed into the
+               engine.templatesByName = map[string]*GoTemplate{}
+               engine.templateSet = template.New("__root__").Funcs(TemplateFuncs)
+               // Check to see what should be used for case sensitivity
+               engine.CaseInsensitive = Config.BoolDefault("go.template.caseinsensitive", true)
+       }
+}
diff --git a/src/foundation/api/revel/template_engine.go b/src/foundation/api/revel/template_engine.go
new file mode 100644 (file)
index 0000000..a62f5f2
--- /dev/null
@@ -0,0 +1,118 @@
+package revel
+
+import (
+       "bufio"
+       "bytes"
+       "errors"
+       "fmt"
+       "path/filepath"
+       "strings"
+)
+
+type TemplateEngine interface {
+       // prase template string and add template to the set.
+       ParseAndAdd(basePath *TemplateView) error
+
+       // returns Template corresponding to the given templateName, or nil
+       Lookup(templateName string) Template
+
+       // Fired by the template loader when events occur
+       Event(event Event, arg interface{})
+
+       // returns true if this engine should be used to parse the file specified in baseTemplate
+       Handles(templateView *TemplateView) bool
+
+       // returns the name of the engine
+       Name() string
+}
+
+// The template view information
+type TemplateView struct {
+       TemplateName string // The name of the view
+       FilePath     string // The file path (view relative)
+       BasePath     string // The file system base path
+       FileBytes    []byte // The file loaded
+       EngineType   string // The name of the engine used to render the view
+}
+
+var templateLoaderMap = map[string]func(loader *TemplateLoader) (TemplateEngine, error){}
+
+// Allow for templates to be registered during init but not initialized until application has been started
+func RegisterTemplateLoader(key string, loader func(loader *TemplateLoader) (TemplateEngine, error)) (err error) {
+       if _, found := templateLoaderMap[key]; found {
+               err = fmt.Errorf("Template loader %s already exists", key)
+       }
+       templateLog.Debug("Registered template engine loaded", "name", key)
+       templateLoaderMap[key] = loader
+       return
+}
+
+// Sets the template name from Config
+// Sets the template API methods for parsing and storing templates before rendering
+func (loader *TemplateLoader) CreateTemplateEngine(templateEngineName string) (TemplateEngine, error) {
+       if "" == templateEngineName {
+               templateEngineName = GO_TEMPLATE
+       }
+       factory := templateLoaderMap[templateEngineName]
+       if nil == factory {
+               fmt.Printf("registered factories %#v\n %s \n", templateLoaderMap, templateEngineName)
+               return nil, errors.New("Unknown template engine name - " + templateEngineName + ".")
+       }
+       templateEngine, err := factory(loader)
+       if nil != err {
+               return nil, errors.New("Failed to init template engine (" + templateEngineName + "), " + err.Error())
+       }
+
+       templateLog.Debug("CreateTemplateEngine: init templates", "name", templateEngineName)
+       return templateEngine, nil
+}
+
+// Passing in a comma delimited list of engine names to be used with this loader to parse the template files
+func (loader *TemplateLoader) initializeEngines(runtimeLoader *templateRuntime, templateEngineNameList string) (err *Error) {
+       // Walk through the template loader's paths and build up a template set.
+       if templateEngineNameList == "" {
+               templateEngineNameList = GO_TEMPLATE
+
+       }
+       runtimeLoader.templatesAndEngineList = []TemplateEngine{}
+       for _, engine := range strings.Split(templateEngineNameList, ",") {
+               engine := strings.TrimSpace(strings.ToLower(engine))
+
+               if templateLoader, err := loader.CreateTemplateEngine(engine); err != nil {
+                       runtimeLoader.compileError = &Error{
+                               Title:       "Panic (Template Loader)",
+                               Description: err.Error(),
+                       }
+                       return runtimeLoader.compileError
+               } else {
+                       // Always assign a default engine, switch it if it is specified in the config
+                       runtimeLoader.templatesAndEngineList = append(runtimeLoader.templatesAndEngineList, templateLoader)
+               }
+       }
+       return
+}
+
+func EngineHandles(engine TemplateEngine, templateView *TemplateView) bool {
+       if line, _, e := bufio.NewReader(bytes.NewBuffer(templateView.FileBytes)).ReadLine(); e == nil && string(line[:3]) == "#! " {
+               // Extract the shebang and look at the rest of the line
+               // #! pong2
+               // #! go
+               templateType := strings.TrimSpace(string(line[2:]))
+               if engine.Name() == templateType {
+                       // Advance the read file bytes so it does not include the shebang
+                       templateView.FileBytes = templateView.FileBytes[len(line)+1:]
+                       templateView.EngineType = templateType
+                       return true
+               }
+       }
+       filename := filepath.Base(templateView.FilePath)
+       bits := strings.Split(filename, ".")
+       if len(bits) > 2 {
+               templateType := strings.TrimSpace(bits[len(bits)-2])
+               if engine.Name() == templateType {
+                       templateView.EngineType = templateType
+                       return true
+               }
+       }
+       return false
+}
diff --git a/src/foundation/api/revel/template_functions.go b/src/foundation/api/revel/template_functions.go
new file mode 100644 (file)
index 0000000..98bb988
--- /dev/null
@@ -0,0 +1,341 @@
+package revel
+
+import (
+       "bytes"
+       "errors"
+       "fmt"
+       "github.com/xeonx/timeago"
+       "html"
+       "html/template"
+       "reflect"
+       "strings"
+       "time"
+)
+
+var (
+       // The functions available for use in the templates.
+       TemplateFuncs = map[string]interface{}{
+               "url": ReverseURL,
+               "set": func(viewArgs map[string]interface{}, key string, value interface{}) template.JS {
+                       viewArgs[key] = value
+                       return template.JS("")
+               },
+               "append": func(viewArgs map[string]interface{}, key string, value interface{}) template.JS {
+                       if viewArgs[key] == nil {
+                               viewArgs[key] = []interface{}{value}
+                       } else {
+                               viewArgs[key] = append(viewArgs[key].([]interface{}), value)
+                       }
+                       return template.JS("")
+               },
+               "field": NewField,
+               "firstof": func(args ...interface{}) interface{} {
+                       for _, val := range args {
+                               switch val.(type) {
+                               case nil:
+                                       continue
+                               case string:
+                                       if val == "" {
+                                               continue
+                                       }
+                                       return val
+                               default:
+                                       return val
+                               }
+                       }
+                       return nil
+               },
+               "option": func(f *Field, val interface{}, label string) template.HTML {
+                       selected := ""
+                       if f.Flash() == val || (f.Flash() == "" && f.Value() == val) {
+                               selected = " selected"
+                       }
+
+                       return template.HTML(fmt.Sprintf(`<option value="%s"%s>%s</option>`,
+                               html.EscapeString(fmt.Sprintf("%v", val)), selected, html.EscapeString(label)))
+               },
+               "radio": func(f *Field, val string) template.HTML {
+                       checked := ""
+                       if f.Flash() == val {
+                               checked = " checked"
+                       }
+                       return template.HTML(fmt.Sprintf(`<input type="radio" name="%s" value="%s"%s>`,
+                               html.EscapeString(f.Name), html.EscapeString(val), checked))
+               },
+               "checkbox": func(f *Field, val string) template.HTML {
+                       checked := ""
+                       if f.Flash() == val {
+                               checked = " checked"
+                       }
+                       return template.HTML(fmt.Sprintf(`<input type="checkbox" name="%s" value="%s"%s>`,
+                               html.EscapeString(f.Name), html.EscapeString(val), checked))
+               },
+               // Pads the given string with &nbsp;'s up to the given width.
+               "pad": func(str string, width int) template.HTML {
+                       if len(str) >= width {
+                               return template.HTML(html.EscapeString(str))
+                       }
+                       return template.HTML(html.EscapeString(str) + strings.Repeat("&nbsp;", width-len(str)))
+               },
+
+               "errorClass": func(name string, viewArgs map[string]interface{}) template.HTML {
+                       errorMap, ok := viewArgs["errors"].(map[string]*ValidationError)
+                       if !ok || errorMap == nil {
+                               templateLog.Warn("errorClass: Called 'errorClass' without 'errors' in the view args.")
+                               return template.HTML("")
+                       }
+                       valError, ok := errorMap[name]
+                       if !ok || valError == nil {
+                               return template.HTML("")
+                       }
+                       return template.HTML(ErrorCSSClass)
+               },
+
+               "msg": func(viewArgs map[string]interface{}, message string, args ...interface{}) template.HTML {
+                       str, ok := viewArgs[CurrentLocaleViewArg].(string)
+                       if !ok {
+                               return ""
+                       }
+                       return template.HTML(MessageFunc(str, message, args...))
+               },
+
+               // Replaces newlines with <br>
+               "nl2br": func(text string) template.HTML {
+                       return template.HTML(strings.Replace(template.HTMLEscapeString(text), "\n", "<br>", -1))
+               },
+
+               // Skips sanitation on the parameter.  Do not use with dynamic data.
+               "raw": func(text string) template.HTML {
+                       return template.HTML(text)
+               },
+
+               // Pluralize, a helper for pluralizing words to correspond to data of dynamic length.
+               // items - a slice of items, or an integer indicating how many items there are.
+               // pluralOverrides - optional arguments specifying the output in the
+               //     singular and plural cases.  by default "" and "s"
+               "pluralize": func(items interface{}, pluralOverrides ...string) string {
+                       singular, plural := "", "s"
+                       if len(pluralOverrides) >= 1 {
+                               singular = pluralOverrides[0]
+                               if len(pluralOverrides) == 2 {
+                                       plural = pluralOverrides[1]
+                               }
+                       }
+
+                       switch v := reflect.ValueOf(items); v.Kind() {
+                       case reflect.Int:
+                               if items.(int) != 1 {
+                                       return plural
+                               }
+                       case reflect.Slice:
+                               if v.Len() != 1 {
+                                       return plural
+                               }
+                       default:
+                               templateLog.Error("pluralize: unexpected type: ", "value", v)
+                       }
+                       return singular
+               },
+
+               // Format a date according to the application's default date(time) format.
+               "date": func(date time.Time) string {
+                       return date.Format(DateFormat)
+               },
+               "datetime": func(date time.Time) string {
+                       return date.Format(DateTimeFormat)
+               },
+               // Fetch an object from the session.
+               "session": func(key string, viewArgs map[string]interface{}) interface{} {
+                       if viewArgs != nil {
+                               if c, found := viewArgs["_controller"]; found {
+                                       if v, err := c.(*Controller).Session.Get(key); err == nil {
+                                               return v
+                                       } else {
+                                               templateLog.Errorf("template.session, key %s error %v", key, err)
+                                       }
+                               } else {
+                                       templateLog.Warnf("template.session, key %s requested without controller", key)
+                               }
+                       } else {
+                               templateLog.Warnf("template.session, key %s requested passing in view args", key)
+                       }
+                       return ""
+               },
+
+               "slug": Slug,
+               "even": func(a int) bool { return (a % 2) == 0 },
+
+               // Using https://github.com/xeonx/timeago
+               "timeago": TimeAgo,
+               "i18ntemplate": func(args ...interface{}) (template.HTML, error) {
+                       templateName, lang := "", ""
+                       var viewArgs interface{}
+                       switch len(args) {
+                       case 0:
+                               templateLog.Error("i18ntemplate: No arguments passed to template call")
+                       case 1:
+                               // Assume only the template name is passed in
+                               templateName = args[0].(string)
+                       case 2:
+                               // Assume template name and viewArgs is passed in
+                               templateName = args[0].(string)
+                               viewArgs = args[1]
+                               // Try to extract language from the view args
+                               if viewargsmap, ok := viewArgs.(map[string]interface{}); ok {
+                                       lang, _ = viewargsmap[CurrentLocaleViewArg].(string)
+                               }
+                       default:
+                               // Assume third argument is the region
+                               templateName = args[0].(string)
+                               viewArgs = args[1]
+                               lang, _ = args[2].(string)
+                               if len(args) > 3 {
+                                       templateLog.Error("i18ntemplate: Received more parameters then needed for", "template", templateName)
+                               }
+                       }
+
+                       var buf bytes.Buffer
+                       // Get template
+                       tmpl, err := MainTemplateLoader.TemplateLang(templateName, lang)
+                       if err == nil {
+                               err = tmpl.Render(&buf, viewArgs)
+                       } else {
+                               templateLog.Error("i18ntemplate: Failed to render i18ntemplate ", "name", templateName, "error", err)
+                       }
+                       return template.HTML(buf.String()), err
+               },
+       }
+)
+
+/////////////////////
+// Template functions
+/////////////////////
+
+// ReverseURL returns a url capable of invoking a given controller method:
+// "Application.ShowApp 123" => "/app/123"
+func ReverseURL(args ...interface{}) (template.URL, error) {
+       if len(args) == 0 {
+               return "", errors.New("no arguments provided to reverse route")
+       }
+
+       action := args[0].(string)
+       if action == "Root" {
+               return template.URL(AppRoot), nil
+       }
+
+       pathData, found := splitActionPath(nil, action, true)
+
+       if !found {
+               return "", fmt.Errorf("reversing '%s', expected 'Controller.Action'", action)
+       }
+
+       // Look up the types.
+
+       if pathData.TypeOfController == nil {
+               return "", fmt.Errorf("Failed reversing %s: controller not found %#v", action, pathData)
+       }
+
+       // Note method name is case insensitive search
+       methodType := pathData.TypeOfController.Method(pathData.MethodName)
+       if methodType == nil {
+               return "", errors.New("revel/controller: In " + action + " failed to find function " + pathData.MethodName)
+       }
+
+       if len(methodType.Args) < len(args)-1 {
+               return "", fmt.Errorf("reversing %s: route defines %d args, but received %d",
+                       action, len(methodType.Args), len(args)-1)
+       }
+       // Unbind the arguments.
+       argsByName := make(map[string]string)
+       // Bind any static args first
+       fixedParams := len(pathData.FixedParamsByName)
+
+       for i, argValue := range args[1:] {
+               Unbind(argsByName, methodType.Args[i+fixedParams].Name, argValue)
+       }
+
+       return template.URL(MainRouter.Reverse(args[0].(string), argsByName).URL), nil
+}
+
+func Slug(text string) string {
+       separator := "-"
+       text = strings.ToLower(text)
+       text = invalidSlugPattern.ReplaceAllString(text, "")
+       text = whiteSpacePattern.ReplaceAllString(text, separator)
+       text = strings.Trim(text, separator)
+       return text
+}
+
+var timeAgoLangs = map[string]timeago.Config{}
+
+func TimeAgo(args ...interface{}) string {
+
+       datetime := time.Now()
+       lang := ""
+       var viewArgs interface{}
+       switch len(args) {
+       case 0:
+               templateLog.Error("TimeAgo: No arguments passed to timeago")
+       case 1:
+               // only the time is passed in
+               datetime = args[0].(time.Time)
+       case 2:
+               // time and region is passed in
+               datetime = args[0].(time.Time)
+               switch v := reflect.ValueOf(args[1]); v.Kind() {
+               case reflect.String:
+                       // second params type string equals region
+                       lang, _ = args[1].(string)
+               case reflect.Map:
+                       // second params type map equals viewArgs
+                       viewArgs = args[1]
+                       if viewargsmap, ok := viewArgs.(map[string]interface{}); ok {
+                               lang, _ = viewargsmap[CurrentLocaleViewArg].(string)
+                       }
+               default:
+                       templateLog.Error("TimeAgo: unexpected type: ", "value", v)
+               }
+       default:
+               // Assume third argument is the region
+               datetime = args[0].(time.Time)
+               if reflect.ValueOf(args[1]).Kind() != reflect.Map {
+                       templateLog.Error("TimeAgo: unexpected type", "value", args[1])
+               }
+               if reflect.ValueOf(args[2]).Kind() != reflect.String {
+                       templateLog.Error("TimeAgo: unexpected type: ", "value", args[2])
+               }
+               viewArgs = args[1]
+               lang, _ = args[2].(string)
+               if len(args) > 3 {
+                       templateLog.Error("TimeAgo: Received more parameters then needed for timeago")
+               }
+       }
+       if lang == "" {
+               lang, _ = Config.String(defaultLanguageOption)
+               if lang == "en" {
+                       timeAgoLangs[lang] = timeago.English
+               }
+       }
+       _, ok := timeAgoLangs[lang]
+       if !ok {
+               timeAgoLangs[lang] = timeago.Config{
+                       PastPrefix:   "",
+                       PastSuffix:   " " + MessageFunc(lang, "ago"),
+                       FuturePrefix: MessageFunc(lang, "in") + " ",
+                       FutureSuffix: "",
+                       Periods: []timeago.FormatPeriod{
+                               {time.Second, MessageFunc(lang, "about a second"), MessageFunc(lang, "%d seconds")},
+                               {time.Minute, MessageFunc(lang, "about a minute"), MessageFunc(lang, "%d minutes")},
+                               {time.Hour, MessageFunc(lang, "about an hour"), MessageFunc(lang, "%d hours")},
+                               {timeago.Day, MessageFunc(lang, "one day"), MessageFunc(lang, "%d days")},
+                               {timeago.Month, MessageFunc(lang, "one month"), MessageFunc(lang, "%d months")},
+                               {timeago.Year, MessageFunc(lang, "one year"), MessageFunc(lang, "%d years")},
+                       },
+                       Zero:          MessageFunc(lang, "about a second"),
+                       Max:           73 * time.Hour,
+                       DefaultLayout: "2006-01-02",
+               }
+
+       }
+       return timeAgoLangs[lang].Format(datetime)
+}
diff --git a/src/foundation/api/revel/templates/errors/403.html b/src/foundation/api/revel/templates/errors/403.html
new file mode 100644 (file)
index 0000000..23e01a2
--- /dev/null
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html lang="en">
+       <head>
+               <title>Forbidden</title>
+       </head>
+       <body>
+       {{with .Error}}
+       <h1>
+               {{.Title}}
+       </h1>
+       <p>
+               {{.Description}}
+       </p>
+       {{end}}
+       </body>
+</html>
diff --git a/src/foundation/api/revel/templates/errors/403.json b/src/foundation/api/revel/templates/errors/403.json
new file mode 100644 (file)
index 0000000..13e1d15
--- /dev/null
@@ -0,0 +1,4 @@
+{
+    "title": "{{js .Error.Title}}",
+    "description": "{{js .Error.Description}}"
+}
diff --git a/src/foundation/api/revel/templates/errors/403.txt b/src/foundation/api/revel/templates/errors/403.txt
new file mode 100644 (file)
index 0000000..cbfc955
--- /dev/null
@@ -0,0 +1,3 @@
+{{.Error.Title}}
+
+{{.Error.Description}}
diff --git a/src/foundation/api/revel/templates/errors/403.xml b/src/foundation/api/revel/templates/errors/403.xml
new file mode 100644 (file)
index 0000000..0696e50
--- /dev/null
@@ -0,0 +1 @@
+<forbidden>{{.Error.Description}}</forbidden>
diff --git a/src/foundation/api/revel/templates/errors/404-dev.html b/src/foundation/api/revel/templates/errors/404-dev.html
new file mode 100644 (file)
index 0000000..e5f0e06
--- /dev/null
@@ -0,0 +1,63 @@
+<style type="text/css">
+       html, body {
+               margin: 0;
+               padding: 0;
+               font-family: Helvetica, Arial, Sans;
+               background: #EEEEEE;
+       }
+       .block {
+               padding: 20px;
+               border-bottom: 1px solid #aaa;
+       }
+       #header h1 {
+               font-weight: normal;
+               font-size: 28px;
+               margin: 0;
+       }
+       #more {
+               color: #666;
+               font-size: 80%;
+               border: none;
+       }
+       #header {
+               background: #FFFFCC;
+       }
+       #header p {
+               color: #333;
+       }
+       #routes {
+               background: #f6f6f6;
+       }
+       #routes h2 {
+               font-weight: normal;
+               font-size: 18px;
+               margin: 0 0 10px 0;
+       }
+       #routes ol {
+
+       }
+       #routes li {
+               font-size: 14px;
+               font-family: monospace;
+               color: #333;
+       }
+</style>
+
+<div id="header" class="block">
+       {{with .Error}}
+       <h1>
+               {{.Title}}
+       </h1>
+       <p>
+               {{.Description}}
+       </p>
+       {{end}}
+</div>
+<div id="routes" class="block">
+       <h2>These routes have been tried, in this order :</h2>
+       <ol>
+               {{range .Router.Routes}}
+                       <li>{{pad .Method 10}}{{pad .Path 50}}{{.Action}} {{with .ModuleSource}}(Route Module:{{.Name}}){{end}}</li>
+               {{end}}
+       </ol>
+</div>
diff --git a/src/foundation/api/revel/templates/errors/404.html b/src/foundation/api/revel/templates/errors/404.html
new file mode 100644 (file)
index 0000000..f548ebb
--- /dev/null
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html lang="en">
+       <head>
+               <title>Not found</title>
+       </head>
+       <body>
+
+{{if .DevMode}}
+
+{{template "errors/404-dev.html" .}}
+
+{{else}}
+
+       {{with .Error}}
+       <h1>
+               {{.Title}}
+       </h1>
+       <p>
+               {{.Description}}
+       </p>
+       {{end}}
+
+{{end}}
+
+       </body>
+</html>
diff --git a/src/foundation/api/revel/templates/errors/404.json b/src/foundation/api/revel/templates/errors/404.json
new file mode 100644 (file)
index 0000000..13e1d15
--- /dev/null
@@ -0,0 +1,4 @@
+{
+    "title": "{{js .Error.Title}}",
+    "description": "{{js .Error.Description}}"
+}
diff --git a/src/foundation/api/revel/templates/errors/404.txt b/src/foundation/api/revel/templates/errors/404.txt
new file mode 100644 (file)
index 0000000..cbfc955
--- /dev/null
@@ -0,0 +1,3 @@
+{{.Error.Title}}
+
+{{.Error.Description}}
diff --git a/src/foundation/api/revel/templates/errors/404.xml b/src/foundation/api/revel/templates/errors/404.xml
new file mode 100644 (file)
index 0000000..29839a5
--- /dev/null
@@ -0,0 +1 @@
+<notfound>{{.Error.Description}}</notfound>
diff --git a/src/foundation/api/revel/templates/errors/405.html b/src/foundation/api/revel/templates/errors/405.html
new file mode 100644 (file)
index 0000000..58f19bc
--- /dev/null
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html lang="en">
+       <head>
+               <title>Method not allowed</title>
+       </head>
+       <body>
+       {{with .Error}}
+       <h1>
+               {{.Title}}
+       </h1>
+       <p>
+               {{.Description}}
+       </p>
+       {{end}}
+       </body>
+</html>
diff --git a/src/foundation/api/revel/templates/errors/405.json b/src/foundation/api/revel/templates/errors/405.json
new file mode 100644 (file)
index 0000000..13e1d15
--- /dev/null
@@ -0,0 +1,4 @@
+{
+    "title": "{{js .Error.Title}}",
+    "description": "{{js .Error.Description}}"
+}
diff --git a/src/foundation/api/revel/templates/errors/405.txt b/src/foundation/api/revel/templates/errors/405.txt
new file mode 100644 (file)
index 0000000..cbfc955
--- /dev/null
@@ -0,0 +1,3 @@
+{{.Error.Title}}
+
+{{.Error.Description}}
diff --git a/src/foundation/api/revel/templates/errors/405.xml b/src/foundation/api/revel/templates/errors/405.xml
new file mode 100644 (file)
index 0000000..c5bea69
--- /dev/null
@@ -0,0 +1 @@
+<method-not-allowed>{{.Error.Description}}</method-not-allowed>
diff --git a/src/foundation/api/revel/templates/errors/500-dev.html b/src/foundation/api/revel/templates/errors/500-dev.html
new file mode 100644 (file)
index 0000000..4f0d46c
--- /dev/null
@@ -0,0 +1,133 @@
+               <style type="text/css">
+               html, body {
+                       margin: 0;
+                       padding: 0;
+                       font-family: Helvetica, Arial, Sans;
+                       background: #EEEEEE;
+               }
+               .block {
+                       padding: 20px;
+                       border-bottom: 1px solid #aaa;
+               }
+               #header h1 {
+                       font-weight: normal;
+                       font-size: 28px;
+                       margin: 0;
+               }
+               #more {
+                       color: #666;
+                       font-size: 80%;
+                       border: none;
+               }
+               #header {
+                       background: #fcd2da;
+               }
+               #header p {
+                       color: #333;
+               }
+               #source {
+                       background: #f6f6f6;
+               }
+               #source h2 {
+                       font-weight: normal;
+                       font-size: 18px;
+                       margin: 0 0 10px 0;
+               }
+               #source .lineNumber {
+                       float: left;
+                       display: block;
+                       width: 40px;
+                       text-align: right;
+                       margin-right: 10px;
+                       font-size: 14px;
+                       font-family: monospace;
+                       background: #333;
+                       color: #fff;
+               }
+               #source .line {
+                       clear: both;
+                       color: #333;
+                       margin-bottom: 1px;
+               }
+               #source pre {
+                       font-size: 14px;
+                       margin: 0;
+                       overflow-x: hidden;
+               }
+               #source .error {
+                       color: #c00 !important;
+               }
+               #source .error .lineNumber {
+                       background: #c00;
+               }
+               #source a {
+                       text-decoration: none;
+               }
+               #source a:hover * {
+                       cursor: pointer !important;
+               }
+               #source a:hover pre {
+                       background: #FAFFCF !important;
+               }
+               #source em {
+                       font-style: normal;
+                       text-decoration: underline;
+                       font-weight: bold;
+               }
+               #source strong {
+                       font-style: normal;
+                       font-weight: bold;
+               }
+               #stack {
+                       background: #eee;
+                       padding:0 1em 1em;
+               }
+               #stack h3 {
+                       font-weight: normal;
+               }
+               #stack code {
+                       font-family:monospace;
+                       white-space: pre;
+               }
+               </style>
+               {{with .Error}}
+               <div id="header" class="block">
+                       <h1>{{.Title}}</h1>
+                       <p>
+                               {{if .SourceType}}
+                                       The {{.SourceType}} <strong>{{.Path}}</strong> does not compile: <strong>{{.Description}}</strong>
+                               {{else}}
+                                       {{.Description}}
+                               {{end}}
+                       </p>
+               </div>
+               {{if .Path}}
+               <div id="source" class="block">
+                       <h2>In {{.Path}}
+                               {{if .Line}}
+                                       (around {{if .Line}}line {{.Line}}{{end}}{{if .Column}} column {{.Column}}{{end}})
+                               {{end}}
+                       </h2>
+                       {{range .ContextSource}}
+                               <div class="line {{if .IsError}}error{{end}}">
+                                       <span class="lineNumber">{{.Line}}:</span>
+                                       <pre>{{.Source}}</pre>
+                               </div>
+                       {{end}}
+               </div>
+               {{end}}
+               {{if .Stack}}
+               <div id="stack">
+                       <h3>Call Stack</h3>
+                       <code>{{.Stack}}</code>
+               </div>
+               {{end}}
+               {{if .MetaError}}
+                       <div id="source" class="block">
+                               <h2>Additionally, an error occurred while handling this error.</h2>
+                               <div class="line error">
+                                       {{.MetaError}}
+                               </div>
+                       </div>
+               {{end}}
+               {{end}}
diff --git a/src/foundation/api/revel/templates/errors/500.html b/src/foundation/api/revel/templates/errors/500.html
new file mode 100644 (file)
index 0000000..ad63527
--- /dev/null
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+       <head>
+               <title>Application error</title>
+       </head>
+       <body>
+               {{if .DevMode}}
+               {{template "errors/500-dev.html" .}}
+               {{else}}
+               <h1>Oops, an error occured</h1>
+               <p>
+                       This exception has been logged.
+               </p>
+               {{end}}
+       </body>
+</html>
diff --git a/src/foundation/api/revel/templates/errors/500.json b/src/foundation/api/revel/templates/errors/500.json
new file mode 100644 (file)
index 0000000..13e1d15
--- /dev/null
@@ -0,0 +1,4 @@
+{
+    "title": "{{js .Error.Title}}",
+    "description": "{{js .Error.Description}}"
+}
diff --git a/src/foundation/api/revel/templates/errors/500.txt b/src/foundation/api/revel/templates/errors/500.txt
new file mode 100644 (file)
index 0000000..dde13f4
--- /dev/null
@@ -0,0 +1,15 @@
+{{.Error.Title}}
+{{.Error.Description}}
+
+{{if eq .RunMode "dev"}}
+{{with .Error}}
+{{if .Path}}
+----------
+In {{.Path}} {{if .Line}}(around line {{.Line}}){{end}}
+
+{{range .ContextSource}}
+{{if .IsError}}>{{else}} {{end}} {{.Line}}: {{.Source}}{{end}}
+
+{{end}}
+{{end}}
+{{end}}
diff --git a/src/foundation/api/revel/templates/errors/500.xml b/src/foundation/api/revel/templates/errors/500.xml
new file mode 100644 (file)
index 0000000..444b4f8
--- /dev/null
@@ -0,0 +1,4 @@
+<error>
+       <title>{{.Error.Title}}</title>
+       <description>{{.Error.Description}}</description>
+</error>
diff --git a/src/foundation/api/revel/testdata/app/views/footer.html b/src/foundation/api/revel/testdata/app/views/footer.html
new file mode 100644 (file)
index 0000000..9307824
--- /dev/null
@@ -0,0 +1,8 @@
+    </div>
+
+    <div id="footer">
+      Created with the <a href="http://github.com/revel/revel">Revel framework</a> and really inspirated from the booking sample application provided by <a href="http://www.playframework.org">play framework</a>, which was really inspired by the booking sample application provided by the <a href="http://seamframework.org/">seam framework</a>.
+    </div>
+
+  </body>
+</html>
diff --git a/src/foundation/api/revel/testdata/app/views/header.html b/src/foundation/api/revel/testdata/app/views/header.html
new file mode 100644 (file)
index 0000000..583bb34
--- /dev/null
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+
+<html>
+  <head>
+    <title>{{.title}}</title>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+    <link rel="stylesheet" type="text/css" media="screen" href="/public/css/main.css">
+    {{range .moreStyles}}
+      <link rel="stylesheet" type="text/css" href="/public/{{.}}">
+    {{end}}
+    <script src="/public/js/jquery-1.3.2.min.js" type="text/javascript" charset="utf-8"></script>
+    <script src="/public/js/sessvars.js" type="text/javascript" charset="utf-8"></script>
+    {{range .moreScripts}}
+      <script src="/public/{{.}}" type="text/javascript" charset="utf-8"></script>
+    {{end}}
+  </head>
+  <body>
+
+    <div id="header">
+      <h1>revel framework booking demo</h1>
+      {{if .user}}
+        <div id="options">
+          Connected as {{.user.Username}}
+          |
+          <a href="{{url "Hotels.Index"}}">Search</a>
+          |
+          <a href="{{url "Hotels.Settings"}}">Settings</a>
+          |
+          <a href="{{url "Application.Logout"}}">Logout</a>
+        </div>
+      {{end}}
+    </div>
+
+    <div id="content">
+      {{if .flash.error}}
+        <p class="fError">
+          <strong>{{.flash.error}}</strong>
+        </p>
+      {{end}}
+      {{if .flash.success}}
+        <p class="fSuccess">
+          <strong>{{.flash.success}}</strong>
+        </p>
+      {{end}}
+
diff --git a/src/foundation/api/revel/testdata/app/views/hotels/show.html b/src/foundation/api/revel/testdata/app/views/hotels/show.html
new file mode 100644 (file)
index 0000000..6d10898
--- /dev/null
@@ -0,0 +1,37 @@
+{{template "header.html" .}}
+
+<h1>View hotel</h1>
+
+{{with .hotel}}
+<form action="{{url "Hotels.Book" .HotelID}}">
+
+  <p>
+    <strong>Name:</strong> {{.Name}}
+  </p>
+  <p>
+    <strong>Address:</strong> {{.Address}}
+  </p>
+  <p>
+    <strong>City:</strong> {{.City}}
+  </p>
+  <p>
+    <strong>State:</strong> {{.State}}
+  </p>
+  <p>
+    <strong>Zip:</strong> {{.Zip}}
+  </p>
+  <p>
+    <strong>Country:</strong> {{.Country}}
+  </p>
+  <p>
+    <strong>Nightly rate:</strong> {{.Price}}
+  </p>
+
+  <p class="buttons">
+    <input type="submit" value="Book Hotel">
+    <a href="{{url "Hotels.Index"}}">Back to search</a>
+  </p>
+</form>
+{{end}}
+
+{{template "footer.html" .}}
diff --git a/src/foundation/api/revel/testdata/conf/app.conf b/src/foundation/api/revel/testdata/conf/app.conf
new file mode 100644 (file)
index 0000000..6efa0cf
--- /dev/null
@@ -0,0 +1,44 @@
+# Application
+app.name=Booking example
+app.secret=secret
+
+# Server
+http.addr=
+http.port=9000
+http.ssl=false
+http.sslcert=
+http.sslkey=
+
+# Logging
+log.trace.output = stderr
+log.info.output  = stderr
+log.warn.output  = stderr
+log.error.output = stderr
+
+log.trace.prefix = "TRACE "
+log.info.prefix  = "INFO  "
+log.warn.prefix  = "WARN  "
+log.error.prefix = "ERROR "
+
+db.import = github.com/mattn/go-sqlite3
+db.driver = sqlite3
+db.spec   = :memory:
+
+build.tags=gorp
+
+# module.jobs=github.com/revel/modules/jobs
+module.static=github.com/revel/modules/static
+
+[dev]
+mode.dev=true
+watch=true
+module.testrunner=github.com/revel/modules/testrunner
+
+[prod]
+watch=false
+module.testrunner=
+
+log.trace.output = off
+log.info.output  = off
+log.warn.output  = stderr
+log.error.output = stderr
diff --git a/src/foundation/api/revel/testdata/conf/routes b/src/foundation/api/revel/testdata/conf/routes
new file mode 100644 (file)
index 0000000..ec40efd
--- /dev/null
@@ -0,0 +1,16 @@
+# Routes
+# This file defines all application routes (Higher priority routes first)
+# ~~~~
+
+module:testrunner
+
+GET     /hotels                                 Hotels.Index
+GET     /hotels/:id                             Hotels.Show
+GET     /hotels/:id/booking                     Hotels.Book
+
+# Map static resources from the /app/public folder to the /public path
+GET     /public/*filepath                       Static.Serve("public")
+GET     /favicon.ico                            Static.Serve("public/img","favicon.png")
+
+# Catch all
+*       /:controller/:action                    :controller.:action
diff --git a/src/foundation/api/revel/testdata/i18n/config/test_app.conf b/src/foundation/api/revel/testdata/i18n/config/test_app.conf
new file mode 100644 (file)
index 0000000..40e12b6
--- /dev/null
@@ -0,0 +1,33 @@
+app.name={{ .AppName }}
+app.secret={{ .Secret }}
+http.addr=
+http.port=9000
+cookie.prefix=REVEL
+
+i18n.default_language=en
+i18n.cookie=APP_LANG
+
+[dev]
+results.pretty=true
+results.staging=true
+watch=true
+
+module.testrunner = github.com/revel/modules/testrunner
+module.static=github.com/revel/modules/static
+
+log.trace.output = off
+log.info.output  = stderr
+log.warn.output  = stderr
+log.error.output = stderr
+
+[prod]
+results.pretty=false
+results.staging=false
+watch=false
+
+module.testrunner =
+
+log.trace.output = off
+log.info.output  = off
+log.warn.output  = %(app.name)s.log
+log.error.output = %(app.name)s.log
diff --git a/src/foundation/api/revel/testdata/i18n/messages/dutch_messages.nl b/src/foundation/api/revel/testdata/i18n/messages/dutch_messages.nl
new file mode 100644 (file)
index 0000000..2e75b47
--- /dev/null
@@ -0,0 +1,9 @@
+greeting=Hallo 
+greeting.name=Rob
+greeting.suffix=, welkom bij Revel!
+
+[NL]
+greeting=Goeiedag
+
+[BE]
+greeting=Hallokes
diff --git a/src/foundation/api/revel/testdata/i18n/messages/english_messages.en b/src/foundation/api/revel/testdata/i18n/messages/english_messages.en
new file mode 100644 (file)
index 0000000..653bfd9
--- /dev/null
@@ -0,0 +1,21 @@
+greeting=Hello 
+greeting.name=Rob
+greeting.suffix=, welcome to Revel!
+
+folded=Greeting is '%(greeting)s'
+folded.arguments=%(greeting.name)s is %d years old
+
+arguments.string=My name is %s
+arguments.hex=The number %d in hexadecimal notation would be %x
+arguments.none=No arguments here son
+
+only_exists_in_default=Default
+
+[AU]
+greeting=G'day
+
+[US]
+greeting=Howdy
+
+[GB]
+greeting=All right
\ No newline at end of file
diff --git a/src/foundation/api/revel/testdata/i18n/messages/english_messages2.en b/src/foundation/api/revel/testdata/i18n/messages/english_messages2.en
new file mode 100644 (file)
index 0000000..ea74539
--- /dev/null
@@ -0,0 +1 @@
+greeting2=Yo!
diff --git a/src/foundation/api/revel/testdata/i18n/messages/invalid_message_file_name.txt b/src/foundation/api/revel/testdata/i18n/messages/invalid_message_file_name.txt
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/foundation/api/revel/testdata/public/js/sessvars.js b/src/foundation/api/revel/testdata/public/js/sessvars.js
new file mode 100644 (file)
index 0000000..9eaafea
--- /dev/null
@@ -0,0 +1 @@
+console.log('Test file');
diff --git a/src/foundation/api/revel/testing/testsuite.go b/src/foundation/api/revel/testing/testsuite.go
new file mode 100644 (file)
index 0000000..ff02472
--- /dev/null
@@ -0,0 +1,411 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package testing
+
+import (
+       "bytes"
+       "fmt"
+       "io"
+       "io/ioutil"
+       "mime"
+       "mime/multipart"
+       "net/http"
+       "net/http/cookiejar"
+       "net/textproto"
+       "net/url"
+       "os"
+       "path/filepath"
+       "regexp"
+       "strings"
+
+       "github.com/revel/revel"
+
+       "github.com/revel/revel/session"
+       "golang.org/x/net/websocket"
+       "net/http/httptest"
+)
+
+type TestSuite struct {
+       Client        *http.Client
+       Response      *http.Response
+       ResponseBody  []byte
+       Session       session.Session
+       SessionEngine revel.SessionEngine
+}
+
+type TestRequest struct {
+       *http.Request
+       testSuite *TestSuite
+}
+
+// This is populated by the generated code in the run/run/go file
+var TestSuites []interface{} // Array of structs that embed TestSuite
+
+// NewTestSuite returns an initialized TestSuite ready for use. It is invoked
+// by the test harness to initialize the embedded field in application tests.
+func NewTestSuite() TestSuite {
+       return NewTestSuiteEngine(revel.NewSessionCookieEngine())
+}
+
+// Define a new test suite with a custom session engine
+func NewTestSuiteEngine(engine revel.SessionEngine) TestSuite {
+       jar, _ := cookiejar.New(nil)
+       ts := TestSuite{
+               Client:        &http.Client{Jar: jar},
+               Session:       session.NewSession(),
+               SessionEngine: engine,
+       }
+
+       return ts
+}
+
+// NewTestRequest returns an initialized *TestRequest. It is used for extending
+// testsuite package making it possibe to define own methods. Example:
+//     type MyTestSuite struct {
+//             testing.TestSuite
+//     }
+//
+//     func (t *MyTestSuite) PutFormCustom(...) {
+//             req := http.NewRequest(...)
+//             ...
+//             return t.NewTestRequest(req)
+//     }
+func (t *TestSuite) NewTestRequest(req *http.Request) *TestRequest {
+       request := &TestRequest{
+               Request:   req,
+               testSuite: t,
+       }
+       return request
+}
+
+// Host returns the address and port of the server, e.g. "127.0.0.1:8557"
+func (t *TestSuite) Host() string {
+       if revel.ServerEngineInit.Address[0] == ':' {
+               return "127.0.0.1" + revel.ServerEngineInit.Address
+       }
+       return revel.ServerEngineInit.Address
+}
+
+// BaseUrl returns the base http/https URL of the server, e.g. "http://127.0.0.1:8557".
+// The scheme is set to https if http.ssl is set to true in the configuration file.
+func (t *TestSuite) BaseUrl() string {
+       if revel.HTTPSsl {
+               return "https://" + t.Host()
+       }
+       return "http://" + t.Host()
+}
+
+// WebSocketUrl returns the base websocket URL of the server, e.g. "ws://127.0.0.1:8557"
+func (t *TestSuite) WebSocketUrl() string {
+       return "ws://" + t.Host()
+}
+
+// Get issues a GET request to the given path and stores the result in Response
+// and ResponseBody.
+func (t *TestSuite) Get(path string) {
+       t.GetCustom(t.BaseUrl() + path).Send()
+}
+
+// GetCustom returns a GET request to the given URI in a form of its wrapper.
+func (t *TestSuite) GetCustom(uri string) *TestRequest {
+       req, err := http.NewRequest("GET", uri, nil)
+       if err != nil {
+               panic(err)
+       }
+       return t.NewTestRequest(req)
+}
+
+// Delete issues a DELETE request to the given path and stores the result in
+// Response and ResponseBody.
+func (t *TestSuite) Delete(path string) {
+       t.DeleteCustom(t.BaseUrl() + path).Send()
+}
+
+// DeleteCustom returns a DELETE request to the given URI in a form of its
+// wrapper.
+func (t *TestSuite) DeleteCustom(uri string) *TestRequest {
+       req, err := http.NewRequest("DELETE", uri, nil)
+       if err != nil {
+               panic(err)
+       }
+       return t.NewTestRequest(req)
+}
+
+// Put issues a PUT request to the given path, sending the given Content-Type
+// and data, storing the result in Response and ResponseBody. "data" may be nil.
+func (t *TestSuite) Put(path string, contentType string, reader io.Reader) {
+       t.PutCustom(t.BaseUrl()+path, contentType, reader).Send()
+}
+
+// PutCustom returns a PUT request to the given URI with specified Content-Type
+// and data in a form of wrapper. "data" may be nil.
+func (t *TestSuite) PutCustom(uri string, contentType string, reader io.Reader) *TestRequest {
+       req, err := http.NewRequest("PUT", uri, reader)
+       if err != nil {
+               panic(err)
+       }
+       req.Header.Set("Content-Type", contentType)
+       return t.NewTestRequest(req)
+}
+
+// PutForm issues a PUT request to the given path as a form put of the given key
+// and values, and stores the result in Response and ResponseBody.
+func (t *TestSuite) PutForm(path string, data url.Values) {
+       t.PutFormCustom(t.BaseUrl()+path, data).Send()
+}
+
+// PutFormCustom returns a PUT request to the given URI as a form put of the
+// given key and values. The request is in a form of TestRequest wrapper.
+func (t *TestSuite) PutFormCustom(uri string, data url.Values) *TestRequest {
+       return t.PutCustom(uri, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
+}
+
+// Patch issues a PATCH request to the given path, sending the given
+// Content-Type and data, and stores the result in Response and ResponseBody.
+// "data" may be nil.
+func (t *TestSuite) Patch(path string, contentType string, reader io.Reader) {
+       t.PatchCustom(t.BaseUrl()+path, contentType, reader).Send()
+}
+
+// PatchCustom returns a PATCH request to the given URI with specified
+// Content-Type and data in a form of wrapper. "data" may be nil.
+func (t *TestSuite) PatchCustom(uri string, contentType string, reader io.Reader) *TestRequest {
+       req, err := http.NewRequest("PATCH", uri, reader)
+       if err != nil {
+               panic(err)
+       }
+       req.Header.Set("Content-Type", contentType)
+       return t.NewTestRequest(req)
+}
+
+// Post issues a POST request to the given path, sending the given Content-Type
+// and data, storing the result in Response and ResponseBody. "data" may be nil.
+func (t *TestSuite) Post(path string, contentType string, reader io.Reader) {
+       t.PostCustom(t.BaseUrl()+path, contentType, reader).Send()
+}
+
+// PostCustom returns a POST request to the given URI with specified
+// Content-Type and data in a form of wrapper. "data" may be nil.
+func (t *TestSuite) PostCustom(uri string, contentType string, reader io.Reader) *TestRequest {
+       req, err := http.NewRequest("POST", uri, reader)
+       if err != nil {
+               panic(err)
+       }
+       req.Header.Set("Content-Type", contentType)
+       return t.NewTestRequest(req)
+}
+
+// PostForm issues a POST request to the given path as a form post of the given
+// key and values, and stores the result in Response and ResponseBody.
+func (t *TestSuite) PostForm(path string, data url.Values) {
+       t.PostFormCustom(t.BaseUrl()+path, data).Send()
+}
+
+// PostFormCustom returns a POST request to the given URI as a form post of the
+// given key and values. The request is in a form of TestRequest wrapper.
+func (t *TestSuite) PostFormCustom(uri string, data url.Values) *TestRequest {
+       return t.PostCustom(uri, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
+}
+
+// PostFile issues a multipart request to the given path sending given params
+// and files, and stores the result in Response and ResponseBody.
+func (t *TestSuite) PostFile(path string, params url.Values, filePaths url.Values) {
+       t.PostFileCustom(t.BaseUrl()+path, params, filePaths).Send()
+}
+
+// PostFileCustom returns a multipart request to the given URI in a form of its
+// wrapper with the given params and files.
+func (t *TestSuite) PostFileCustom(uri string, params url.Values, filePaths url.Values) *TestRequest {
+       body := &bytes.Buffer{}
+       writer := multipart.NewWriter(body)
+
+       for key, values := range filePaths {
+               for _, value := range values {
+                       createFormFile(writer, key, value)
+               }
+       }
+
+       for key, values := range params {
+               for _, value := range values {
+                       err := writer.WriteField(key, value)
+                       t.AssertEqual(nil, err)
+               }
+       }
+       err := writer.Close()
+       t.AssertEqual(nil, err)
+
+       return t.PostCustom(uri, writer.FormDataContentType(), body)
+}
+
+// Send issues any request and reads the response. If successful, the caller may
+// examine the Response and ResponseBody properties. Session data will be
+// added.
+func (r *TestRequest) Send() {
+       writer := httptest.NewRecorder()
+       context := revel.NewGoContext(nil)
+       context.Request.SetRequest(r.Request)
+       context.Response.SetResponse(writer)
+       controller := revel.NewController(context)
+       controller.Session = r.testSuite.Session
+
+       r.testSuite.SessionEngine.Encode(controller)
+       response := http.Response{Header: writer.Header()}
+       cookies := response.Cookies()
+       for _, c := range cookies {
+               r.AddCookie(c)
+       }
+       r.MakeRequest()
+}
+
+// MakeRequest issues any request and read the response. If successful, the
+// caller may examine the Response and ResponseBody properties. You will need to
+// manage session / cookie data manually
+func (r *TestRequest) MakeRequest() {
+       var err error
+       if r.testSuite.Response, err = r.testSuite.Client.Do(r.Request); err != nil {
+               panic(err)
+       }
+       if r.testSuite.ResponseBody, err = ioutil.ReadAll(r.testSuite.Response.Body); err != nil {
+               panic(err)
+       }
+
+       // Create the controller again to receive the response for processing.
+       context := revel.NewGoContext(nil)
+       // Set the request with the header from the response..
+       newRequest := &http.Request{URL: r.URL, Header: r.testSuite.Response.Header}
+       for _, cookie := range r.testSuite.Client.Jar.Cookies(r.Request.URL) {
+               newRequest.AddCookie(cookie)
+       }
+       context.Request.SetRequest(newRequest)
+       context.Response.SetResponse(httptest.NewRecorder())
+       controller := revel.NewController(context)
+
+       // Decode the session data from the controller and assign it to the session
+       r.testSuite.SessionEngine.Decode(controller)
+       r.testSuite.Session = controller.Session
+}
+
+// WebSocket creates a websocket connection to the given path and returns it
+func (t *TestSuite) WebSocket(path string) *websocket.Conn {
+       origin := t.BaseUrl() + "/"
+       urlPath := t.WebSocketUrl() + path
+       ws, err := websocket.Dial(urlPath, "", origin)
+       if err != nil {
+               panic(err)
+       }
+       return ws
+}
+
+func (t *TestSuite) AssertOk() {
+       t.AssertStatus(http.StatusOK)
+}
+
+func (t *TestSuite) AssertNotFound() {
+       t.AssertStatus(http.StatusNotFound)
+}
+
+func (t *TestSuite) AssertStatus(status int) {
+       if t.Response.StatusCode != status {
+               panic(fmt.Errorf("Status: (expected) %d != %d (actual)", status, t.Response.StatusCode))
+       }
+}
+
+func (t *TestSuite) AssertContentType(contentType string) {
+       t.AssertHeader("Content-Type", contentType)
+}
+
+func (t *TestSuite) AssertHeader(name, value string) {
+       actual := t.Response.Header.Get(name)
+       if actual != value {
+               panic(fmt.Errorf("Header %s: (expected) %s != %s (actual)", name, value, actual))
+       }
+}
+
+func (t *TestSuite) AssertEqual(expected, actual interface{}) {
+       if !revel.Equal(expected, actual) {
+               panic(fmt.Errorf("(expected) %v != %v (actual)", expected, actual))
+       }
+}
+
+func (t *TestSuite) AssertNotEqual(expected, actual interface{}) {
+       if revel.Equal(expected, actual) {
+               panic(fmt.Errorf("(expected) %v == %v (actual)", expected, actual))
+       }
+}
+
+func (t *TestSuite) Assert(exp bool) {
+       t.Assertf(exp, "Assertion failed")
+}
+
+func (t *TestSuite) Assertf(exp bool, formatStr string, args ...interface{}) {
+       if !exp {
+               panic(fmt.Errorf(formatStr, args...))
+       }
+}
+
+// AssertContains asserts that the response contains the given string.
+func (t *TestSuite) AssertContains(s string) {
+       if !bytes.Contains(t.ResponseBody, []byte(s)) {
+               panic(fmt.Errorf("Assertion failed. Expected response to contain %s", s))
+       }
+}
+
+// AssertNotContains asserts that the response does not contain the given string.
+func (t *TestSuite) AssertNotContains(s string) {
+       if bytes.Contains(t.ResponseBody, []byte(s)) {
+               panic(fmt.Errorf("Assertion failed. Expected response not to contain %s", s))
+       }
+}
+
+// AssertContainsRegex asserts that the response matches the given regular expression.
+func (t *TestSuite) AssertContainsRegex(regex string) {
+       r := regexp.MustCompile(regex)
+
+       if !r.Match(t.ResponseBody) {
+               panic(fmt.Errorf("Assertion failed. Expected response to match regexp %s", regex))
+       }
+}
+
+func createFormFile(writer *multipart.Writer, fieldname, filename string) {
+       // Try to open the file.
+       file, err := os.Open(filename)
+       if err != nil {
+               panic(err)
+       }
+       defer func() {
+               _ = file.Close()
+       }()
+
+       // Create a new form-data header with the provided field name and file name.
+       // Determine Content-Type of the file by its extension.
+       h := textproto.MIMEHeader{}
+       h.Set("Content-Disposition", fmt.Sprintf(
+               `form-data; name="%s"; filename="%s"`,
+               escapeQuotes(fieldname),
+               escapeQuotes(filepath.Base(filename)),
+       ))
+       h.Set("Content-Type", "application/octet-stream")
+       if ct := mime.TypeByExtension(filepath.Ext(filename)); ct != "" {
+               h.Set("Content-Type", ct)
+       }
+       part, err := writer.CreatePart(h)
+       if err != nil {
+               panic(err)
+       }
+
+       // Copy the content of the file we have opened not reading the whole
+       // file into memory.
+       _, err = io.Copy(part, file)
+       if err != nil {
+               panic(err)
+       }
+}
+
+var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
+
+// This function was borrowed from mime/multipart package.
+func escapeQuotes(s string) string {
+       return quoteEscaper.Replace(s)
+}
diff --git a/src/foundation/api/revel/testing/testsuite_test.go b/src/foundation/api/revel/testing/testsuite_test.go
new file mode 100644 (file)
index 0000000..e82066c
--- /dev/null
@@ -0,0 +1,303 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package testing
+
+import (
+       "bytes"
+       "fmt"
+       "net/http"
+       "net/http/httptest"
+       "net/url"
+       "os"
+       "path/filepath"
+       "strings"
+       "testing"
+       "time"
+
+       "github.com/revel/revel"
+       "github.com/revel/revel/session"
+)
+
+func TestMisc(t *testing.T) {
+       testSuite := createNewTestSuite(t)
+
+       // test Host value
+       if !strings.EqualFold("127.0.0.1:9001", testSuite.Host()) {
+               t.Error("Incorrect Host value found.")
+       }
+
+       // test BaseUrl
+       if !strings.EqualFold("http://127.0.0.1:9001", testSuite.BaseUrl()) {
+               t.Error("Incorrect BaseUrl http value found.")
+       }
+       revel.HTTPSsl = true
+       if !strings.EqualFold("https://127.0.0.1:9001", testSuite.BaseUrl()) {
+               t.Error("Incorrect BaseUrl https value found.")
+       }
+       revel.HTTPSsl = false
+
+       // test WebSocketUrl
+       if !strings.EqualFold("ws://127.0.0.1:9001", testSuite.WebSocketUrl()) {
+               t.Error("Incorrect WebSocketUrl value found.")
+       }
+
+       testSuite.AssertNotEqual("Yes", "No")
+       testSuite.Assert(true)
+}
+
+func TestGet(t *testing.T) {
+       ts := createTestServer(testHandle)
+       defer ts.Close()
+
+       testSuite := createNewTestSuite(t)
+
+       testSuite.Get("/")
+       testSuite.AssertOk()
+       testSuite.AssertContains("this is testcase homepage")
+       testSuite.AssertNotContains("not exists")
+}
+
+func TestGetNotFound(t *testing.T) {
+       ts := createTestServer(testHandle)
+       defer ts.Close()
+
+       testSuite := createNewTestSuite(t)
+
+       testSuite.Get("/notfound")
+       testSuite.AssertNotFound()
+       // testSuite.AssertContains("this is testcase homepage")
+       // testSuite.AssertNotContains("not exists")
+}
+
+// This test is known to fail
+func TestGetCustom(t *testing.T) {
+       testSuite := createNewTestSuite(t)
+       for x := 0; x < 5; x++ {
+               testSuite.GetCustom("http://httpbin.org/get").Send()
+               if testSuite.Response.StatusCode == http.StatusOK {
+                       break
+               }
+               println("Failed request from http://httpbin.org/get", testSuite.Response.StatusCode)
+       }
+
+       testSuite.AssertOk()
+       testSuite.AssertContentType("application/json")
+       testSuite.AssertContains("httpbin.org")
+       testSuite.AssertContainsRegex("gzip|deflate")
+}
+
+func TestDelete(t *testing.T) {
+       ts := createTestServer(testHandle)
+       defer ts.Close()
+
+       testSuite := createNewTestSuite(t)
+
+       testSuite.Delete("/purchases/10001")
+       testSuite.AssertOk()
+}
+
+func TestPut(t *testing.T) {
+       ts := createTestServer(testHandle)
+       defer ts.Close()
+
+       testSuite := createNewTestSuite(t)
+
+       testSuite.Put("/purchases/10002",
+               "application/json",
+               bytes.NewReader([]byte(`{"sku":"163645GHT", "desc":"This is test product"}`)),
+       )
+       testSuite.AssertStatus(http.StatusNoContent)
+}
+
+func TestPutForm(t *testing.T) {
+       ts := createTestServer(testHandle)
+       defer ts.Close()
+
+       testSuite := createNewTestSuite(t)
+
+       data := url.Values{}
+       data.Add("name", "beacon1name")
+       data.Add("value", "beacon1value")
+
+       testSuite.PutForm("/send", data)
+       testSuite.AssertStatus(http.StatusNoContent)
+}
+
+func TestPatch(t *testing.T) {
+       ts := createTestServer(testHandle)
+       defer ts.Close()
+
+       testSuite := createNewTestSuite(t)
+
+       testSuite.Patch("/purchases/10003",
+               "application/json",
+               bytes.NewReader([]byte(`{"desc": "This is test patch for product"}`)),
+       )
+       testSuite.AssertStatus(http.StatusNoContent)
+}
+
+func TestPost(t *testing.T) {
+       ts := createTestServer(testHandle)
+       defer ts.Close()
+
+       testSuite := createNewTestSuite(t)
+
+       testSuite.Post("/login",
+               "application/json",
+               bytes.NewReader([]byte(`{"username":"testuser", "password":"testpass"}`)),
+       )
+       testSuite.AssertOk()
+       testSuite.AssertContains("login successful")
+}
+
+func TestPostForm(t *testing.T) {
+       ts := createTestServer(testHandle)
+       defer ts.Close()
+
+       testSuite := createNewTestSuite(t)
+
+       data := url.Values{}
+       data.Add("username", "testuser")
+       data.Add("password", "testpassword")
+
+       testSuite.PostForm("/login", data)
+       testSuite.AssertOk()
+       testSuite.AssertContains("login successful")
+}
+
+func TestPostFileUpload(t *testing.T) {
+       ts := createTestServer(testHandle)
+       defer ts.Close()
+
+       testSuite := createNewTestSuite(t)
+
+       params := url.Values{}
+       params.Add("first_name", "Jeevanandam")
+       params.Add("last_name", "M.")
+
+       currentDir, _ := os.Getwd()
+       basePath := filepath.Dir(currentDir)
+
+       filePaths := url.Values{}
+       filePaths.Add("revel_file", filepath.Join(basePath, "revel.go"))
+       filePaths.Add("server_file", filepath.Join(basePath, "server.go"))
+       filePaths.Add("readme_file", filepath.Join(basePath, "README.md"))
+
+       testSuite.PostFile("/upload", params, filePaths)
+
+       testSuite.AssertOk()
+       testSuite.AssertContains("File: revel.go")
+       testSuite.AssertContains("File: server.go")
+       testSuite.AssertNotContains("File: not_exists.go")
+       testSuite.AssertEqual("text/plain; charset=utf-8", testSuite.Response.Header.Get("Content-Type"))
+
+}
+
+func createNewTestSuite(t *testing.T) *TestSuite {
+       suite := NewTestSuite()
+
+       if suite.Client == nil || suite.SessionEngine == nil {
+               t.Error("Unable to create a testsuite")
+       }
+
+       return &suite
+}
+
+func testHandle(w http.ResponseWriter, r *http.Request) {
+       if r.Method == "GET" {
+               if r.URL.Path == "/" {
+                       _, _ = w.Write([]byte(`this is testcase homepage`))
+                       return
+               }
+       }
+
+       if r.Method == "POST" {
+               if r.URL.Path == "/login" {
+                       http.SetCookie(w, &http.Cookie{
+                               Name:     session.SessionCookieSuffix,
+                               Value:    "This is simple session value",
+                               Path:     "/",
+                               HttpOnly: true,
+                               Secure:   false,
+                               Expires:  time.Now().Add(time.Minute * 5).UTC(),
+                       })
+
+                       w.Header().Set("Content-Type", "application/json")
+                       _, _ = w.Write([]byte(`{ "id": "success", "message": "login successful" }`))
+                       return
+               }
+
+               handleFileUpload(w, r)
+               return
+       }
+
+       if r.Method == "DELETE" {
+               if r.URL.Path == "/purchases/10001" {
+                       w.WriteHeader(http.StatusOK)
+                       return
+               }
+       }
+
+       if r.Method == "PUT" {
+               if r.URL.Path == "/purchases/10002" {
+                       w.WriteHeader(http.StatusNoContent)
+                       return
+               }
+
+               if r.URL.Path == "/send" {
+                       w.WriteHeader(http.StatusNoContent)
+                       return
+               }
+       }
+
+       if r.Method == "PATCH" {
+               if r.URL.Path == "/purchases/10003" {
+                       w.WriteHeader(http.StatusNoContent)
+                       return
+               }
+       }
+
+       w.WriteHeader(http.StatusNotFound)
+}
+
+func handleFileUpload(w http.ResponseWriter, r *http.Request) {
+       if r.URL.Path == "/upload" {
+               _ = r.ParseMultipartForm(10e6)
+               var buf bytes.Buffer
+               for _, fhdrs := range r.MultipartForm.File {
+                       for _, hdr := range fhdrs {
+                               dotPos := strings.LastIndex(hdr.Filename, ".")
+                               fname := fmt.Sprintf("%s-%v%s", hdr.Filename[:dotPos], time.Now().Unix(), hdr.Filename[dotPos:])
+                               _, _ = buf.WriteString(fmt.Sprintf(
+                                       "Firstname: %v\nLastname: %v\nFile: %v\nHeader: %v\nUploaded as: %v\n",
+                                       r.FormValue("first_name"),
+                                       r.FormValue("last_name"),
+                                       hdr.Filename,
+                                       hdr.Header,
+                                       fname))
+                       }
+               }
+
+               _, _ = w.Write(buf.Bytes())
+
+               return
+       }
+}
+
+func createTestServer(fn func(w http.ResponseWriter, r *http.Request)) *httptest.Server {
+       testServer := httptest.NewServer(http.HandlerFunc(fn))
+       revel.ServerEngineInit.Address = testServer.URL[7:]
+       return testServer
+}
+
+func init() {
+       if revel.ServerEngineInit == nil {
+               revel.ServerEngineInit = &revel.EngineInit{
+                       Address: ":9001",
+                       Network: "http",
+                       Port:    9001,
+               }
+       }
+}
diff --git a/src/foundation/api/revel/util.go b/src/foundation/api/revel/util.go
new file mode 100644 (file)
index 0000000..340c599
--- /dev/null
@@ -0,0 +1,280 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "bytes"
+       "fmt"
+       "io"
+       "io/ioutil"
+       "net"
+       "net/http"
+       "net/url"
+       "os"
+       "path/filepath"
+       "reflect"
+       "regexp"
+       "strings"
+
+       "github.com/revel/config"
+)
+
+const (
+       // DefaultFileContentType Revel's default response content type
+       DefaultFileContentType = "application/octet-stream"
+)
+
+var (
+       cookieKeyValueParser = regexp.MustCompile("\x00([^:]*):([^\x00]*)\x00")
+       HdrForwardedFor      = http.CanonicalHeaderKey("X-Forwarded-For")
+       HdrRealIP            = http.CanonicalHeaderKey("X-Real-Ip")
+       utilLog              = RevelLog.New("section", "util")
+       mimeConfig           *config.Context
+)
+
+// ExecutableTemplate adds some more methods to the default Template.
+type ExecutableTemplate interface {
+       Execute(io.Writer, interface{}) error
+}
+
+// ExecuteTemplate execute a template and returns the result as a string.
+func ExecuteTemplate(tmpl ExecutableTemplate, data interface{}) string {
+       var b bytes.Buffer
+       if err := tmpl.Execute(&b, data); err != nil {
+               utilLog.Error("ExecuteTemplate: Execute failed", "error", err)
+       }
+       return b.String()
+}
+
+// MustReadLines reads the lines of the given file.  Panics in the case of error.
+func MustReadLines(filename string) []string {
+       r, err := ReadLines(filename)
+       if err != nil {
+               panic(err)
+       }
+       return r
+}
+
+// ReadLines reads the lines of the given file.  Panics in the case of error.
+func ReadLines(filename string) ([]string, error) {
+       dataBytes, err := ioutil.ReadFile(filename)
+       if err != nil {
+               return nil, err
+       }
+       return strings.Split(string(dataBytes), "\n"), nil
+}
+
+func ContainsString(list []string, target string) bool {
+       for _, el := range list {
+               if el == target {
+                       return true
+               }
+       }
+       return false
+}
+
+// FindMethod returns the reflect.Method, given a Receiver type and Func value.
+func FindMethod(recvType reflect.Type, funcVal reflect.Value) *reflect.Method {
+       // It is not possible to get the name of the method from the Func.
+       // Instead, compare it to each method of the Controller.
+       for i := 0; i < recvType.NumMethod(); i++ {
+               method := recvType.Method(i)
+               if method.Func.Pointer() == funcVal.Pointer() {
+                       return &method
+               }
+       }
+       return nil
+}
+
+// ParseKeyValueCookie takes the raw (escaped) cookie value and parses out key values.
+func ParseKeyValueCookie(val string, cb func(key, val string)) {
+       val, _ = url.QueryUnescape(val)
+       if matches := cookieKeyValueParser.FindAllStringSubmatch(val, -1); matches != nil {
+               for _, match := range matches {
+                       cb(match[1], match[2])
+               }
+       }
+}
+
+// LoadMimeConfig load mime-types.conf on init.
+func LoadMimeConfig() {
+       var err error
+       mimeConfig, err = config.LoadContext("mime-types.conf", ConfPaths)
+       if err != nil {
+               utilLog.Fatal("Failed to load mime type config:", "error", err)
+       }
+}
+
+// ContentTypeByFilename returns a MIME content type based on the filename's extension.
+// If no appropriate one is found, returns "application/octet-stream" by default.
+// Additionally, specifies the charset as UTF-8 for text/* types.
+func ContentTypeByFilename(filename string) string {
+       dot := strings.LastIndex(filename, ".")
+       if dot == -1 || dot+1 >= len(filename) {
+               return DefaultFileContentType
+       }
+
+       extension := filename[dot+1:]
+       contentType := mimeConfig.StringDefault(extension, "")
+       if contentType == "" {
+               return DefaultFileContentType
+       }
+
+       if strings.HasPrefix(contentType, "text/") {
+               return contentType + "; charset=utf-8"
+       }
+
+       return contentType
+}
+
+// DirExists returns true if the given path exists and is a directory.
+func DirExists(filename string) bool {
+       fileInfo, err := os.Stat(filename)
+       return err == nil && fileInfo.IsDir()
+}
+
+func FirstNonEmpty(strs ...string) string {
+       for _, str := range strs {
+               if len(str) > 0 {
+                       return str
+               }
+       }
+       return ""
+}
+
+// Equal is a helper for comparing value equality, following these rules:
+//  - Values with equivalent types are compared with reflect.DeepEqual
+//  - int, uint, and float values are compared without regard to the type width.
+//    for example, Equal(int32(5), int64(5)) == true
+//  - strings and byte slices are converted to strings before comparison.
+//  - else, return false.
+func Equal(a, b interface{}) bool {
+       if reflect.TypeOf(a) == reflect.TypeOf(b) {
+               return reflect.DeepEqual(a, b)
+       }
+       switch a.(type) {
+       case int, int8, int16, int32, int64:
+               switch b.(type) {
+               case int, int8, int16, int32, int64:
+                       return reflect.ValueOf(a).Int() == reflect.ValueOf(b).Int()
+               }
+       case uint, uint8, uint16, uint32, uint64:
+               switch b.(type) {
+               case uint, uint8, uint16, uint32, uint64:
+                       return reflect.ValueOf(a).Uint() == reflect.ValueOf(b).Uint()
+               }
+       case float32, float64:
+               switch b.(type) {
+               case float32, float64:
+                       return reflect.ValueOf(a).Float() == reflect.ValueOf(b).Float()
+               }
+       case string:
+               switch b.(type) {
+               case []byte:
+                       return a.(string) == string(b.([]byte))
+               }
+       case []byte:
+               switch b.(type) {
+               case string:
+                       return b.(string) == string(a.([]byte))
+               }
+       }
+       return false
+}
+
+// ClientIP method returns client IP address from HTTP request.
+//
+// Note: Set property "app.behind.proxy" to true only if Revel is running
+// behind proxy like nginx, haproxy, apache, etc. Otherwise
+// you may get inaccurate Client IP address. Revel parses the
+// IP address in the order of X-Forwarded-For, X-Real-IP.
+//
+// By default revel will get http.Request's RemoteAddr
+func ClientIP(r *Request) string {
+       if Config.BoolDefault("app.behind.proxy", false) {
+               // Header X-Forwarded-For
+               if fwdFor := strings.TrimSpace(r.GetHttpHeader(HdrForwardedFor)); fwdFor != "" {
+                       index := strings.Index(fwdFor, ",")
+                       if index == -1 {
+                               return fwdFor
+                       }
+                       return fwdFor[:index]
+               }
+
+               // Header X-Real-Ip
+               if realIP := strings.TrimSpace(r.GetHttpHeader(HdrRealIP)); realIP != "" {
+                       return realIP
+               }
+       }
+
+       if remoteAddr, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
+               return remoteAddr
+       }
+
+       return ""
+}
+
+// Walk method extends filepath.Walk to also follow symlinks.
+// Always returns the path of the file or directory.
+func Walk(root string, walkFn filepath.WalkFunc) error {
+       return fsWalk(root, root, walkFn)
+}
+
+// createDir method creates nested directories if not exists
+func createDir(path string) error {
+       if _, err := os.Stat(path); err != nil {
+               if os.IsNotExist(err) {
+                       if err = os.MkdirAll(path, 0755); err != nil {
+                               return fmt.Errorf("Failed to create directory '%v': %v", path, err)
+                       }
+               } else {
+                       return fmt.Errorf("Failed to create directory '%v': %v", path, err)
+               }
+       }
+       return nil
+}
+
+func fsWalk(fname string, linkName string, walkFn filepath.WalkFunc) error {
+       fsWalkFunc := func(path string, info os.FileInfo, err error) error {
+               if err != nil {
+                       return err
+               }
+
+               var name string
+               name, err = filepath.Rel(fname, path)
+               if err != nil {
+                       return err
+               }
+
+               path = filepath.Join(linkName, name)
+
+               if err == nil && info.Mode()&os.ModeSymlink == os.ModeSymlink {
+                       var symlinkPath string
+                       symlinkPath, err = filepath.EvalSymlinks(path)
+                       if err != nil {
+                               return err
+                       }
+
+                       // https://github.com/golang/go/blob/master/src/path/filepath/path.go#L392
+                       info, err = os.Lstat(symlinkPath)
+
+                       if err != nil {
+                               return walkFn(path, info, err)
+                       }
+
+                       if info.IsDir() {
+                               return fsWalk(symlinkPath, path, walkFn)
+                       }
+               }
+
+               return walkFn(path, info, err)
+       }
+       err := filepath.Walk(fname, fsWalkFunc)
+       return err
+}
+
+func init() {
+       OnAppStart(LoadMimeConfig)
+}
diff --git a/src/foundation/api/revel/util_test.go b/src/foundation/api/revel/util_test.go
new file mode 100644 (file)
index 0000000..96f19ef
--- /dev/null
@@ -0,0 +1,95 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "path/filepath"
+       "reflect"
+       "testing"
+)
+
+func TestContentTypeByFilename(t *testing.T) {
+       testCases := map[string]string{
+               "xyz.jpg":       "image/jpeg",
+               "helloworld.c":  "text/x-c; charset=utf-8",
+               "helloworld.":   "application/octet-stream",
+               "helloworld":    "application/octet-stream",
+               "hello.world.c": "text/x-c; charset=utf-8",
+       }
+       srcPath, _ := findSrcPaths(RevelImportPath)
+       ConfPaths = []string{filepath.Join(
+               srcPath,
+               filepath.FromSlash(RevelImportPath),
+               "conf"),
+       }
+       LoadMimeConfig()
+       for filename, expected := range testCases {
+               actual := ContentTypeByFilename(filename)
+               if actual != expected {
+                       t.Errorf("%s: %s, Expected %s", filename, actual, expected)
+               }
+       }
+}
+
+func TestEqual(t *testing.T) {
+       type testStruct struct{}
+       type testStruct2 struct{}
+       i, i2 := 8, 9
+       s, s2 := "@朕µ\n\tüöäß", "@朕µ\n\tüöäss"
+       slice, slice2 := []int{1, 2, 3, 4, 5}, []int{1, 2, 3, 4, 5}
+       slice3, slice4 := []int{5, 4, 3, 2, 1}, []int{5, 4, 3, 2, 1}
+
+       tm := map[string][]interface{}{
+               "slices":   {slice, slice2},
+               "slices2":  {slice3, slice4},
+               "types":    {new(testStruct), new(testStruct)},
+               "types2":   {new(testStruct2), new(testStruct2)},
+               "ints":     {int(i), int8(i), int16(i), int32(i), int64(i)},
+               "ints2":    {int(i2), int8(i2), int16(i2), int32(i2), int64(i2)},
+               "uints":    {uint(i), uint8(i), uint16(i), uint32(i), uint64(i)},
+               "uints2":   {uint(i2), uint8(i2), uint16(i2), uint32(i2), uint64(i2)},
+               "floats":   {float32(i), float64(i)},
+               "floats2":  {float32(i2), float64(i2)},
+               "strings":  {[]byte(s), s},
+               "strings2": {[]byte(s2), s2},
+       }
+
+       testRow := func(row, row2 string, expected bool) {
+               for _, a := range tm[row] {
+                       for _, b := range tm[row2] {
+                               ok := Equal(a, b)
+                               if ok != expected {
+                                       ak := reflect.TypeOf(a).Kind()
+                                       bk := reflect.TypeOf(b).Kind()
+                                       t.Errorf("eq(%s=%v,%s=%v) want %t got %t", ak, a, bk, b, expected, ok)
+                               }
+                       }
+               }
+       }
+
+       testRow("slices", "slices", true)
+       testRow("slices", "slices2", false)
+       testRow("slices2", "slices", false)
+
+       testRow("types", "types", true)
+       testRow("types2", "types", false)
+       testRow("types", "types2", false)
+
+       testRow("ints", "ints", true)
+       testRow("ints", "ints2", false)
+       testRow("ints2", "ints", false)
+
+       testRow("uints", "uints", true)
+       testRow("uints2", "uints", false)
+       testRow("uints", "uints2", false)
+
+       testRow("floats", "floats", true)
+       testRow("floats2", "floats", false)
+       testRow("floats", "floats2", false)
+
+       testRow("strings", "strings", true)
+       testRow("strings2", "strings", false)
+       testRow("strings", "strings2", false)
+}
diff --git a/src/foundation/api/revel/utils/simplestack.go b/src/foundation/api/revel/utils/simplestack.go
new file mode 100644 (file)
index 0000000..bb22430
--- /dev/null
@@ -0,0 +1,103 @@
+package utils
+
+import (
+       "fmt"
+       "sync"
+)
+
+type (
+       SimpleLockStack struct {
+               Current  *SimpleLockStackElement
+               Creator  func() interface{}
+               len      int
+               capacity int
+               active   int
+               maxsize  int
+               lock     sync.Mutex
+       }
+       SimpleLockStackElement struct {
+               Value    interface{}
+               Previous *SimpleLockStackElement
+               Next     *SimpleLockStackElement
+       }
+       ObjectDestroy interface {
+               Destroy()
+       }
+)
+
+func NewStackLock(startsize, maxsize int, creator func() interface{}) *SimpleLockStack {
+       ss := &SimpleLockStack{lock: sync.Mutex{}, Current: &SimpleLockStackElement{Value: creator()}, Creator: creator, maxsize: maxsize}
+       if startsize > 0 {
+               elements := make([]SimpleLockStackElement, startsize-1)
+               current := ss.Current
+               for i := range elements {
+                       e := elements[i]
+                       if creator != nil {
+                               e.Value = creator()
+                       }
+                       current.Next = &e
+                       e.Previous = current
+                       current = &e
+               }
+               ss.capacity, ss.len, ss.active = startsize, startsize, 0
+
+               ss.Current = current
+       }
+       return ss
+}
+func (s *SimpleLockStack) Pop() (value interface{}) {
+       s.lock.Lock()
+       defer s.lock.Unlock()
+       if s.len == 0 {
+               // Pool is empty, create a new item to return
+               if s.Creator != nil {
+                       value = s.Creator()
+               }
+       } else {
+               value = s.Current.Value
+               s.len--
+               if s.Current.Previous != nil {
+                       s.Current = s.Current.Previous
+               }
+       }
+       // println("Pop ",value, s.len, s.active, s.capacity, s.Current.Next)
+       s.active++
+       return
+}
+func (s *SimpleLockStack) Push(value interface{}) {
+       if d, ok := value.(ObjectDestroy); ok {
+               d.Destroy()
+       }
+       s.lock.Lock()
+       defer s.lock.Unlock()
+       if s.len == 0 {
+               s.Current.Value = value
+       } else if s.len < s.maxsize {
+               if s.Current.Next == nil {
+                       s.Current.Next = &SimpleLockStackElement{Value: value, Previous: s.Current}
+                       s.capacity++
+               } else {
+                       s.Current.Next.Value = value
+               }
+               s.Current = s.Current.Next
+       } else {
+               // If we exceeded the capacity of stack do not store the created object
+               return
+       }
+       s.len++
+       s.active--
+       //println("Push ",value, s.len, s.active, s.capacity)
+       return
+}
+func (s *SimpleLockStack) Len() int {
+       return s.len
+}
+func (s *SimpleLockStack) Capacity() int {
+       return s.capacity
+}
+func (s *SimpleLockStack) Active() int {
+       return s.active
+}
+func (s *SimpleLockStack) String() string {
+       return fmt.Sprintf("SS: Capacity:%d Active:%d Stored:%d", s.capacity, s.active, s.len)
+}
diff --git a/src/foundation/api/revel/utils/simplestack_test.go b/src/foundation/api/revel/utils/simplestack_test.go
new file mode 100644 (file)
index 0000000..3a93740
--- /dev/null
@@ -0,0 +1,127 @@
+package utils
+
+import (
+       "testing"
+)
+
+type SimpleStackTest struct {
+       index int
+}
+
+func TestUnique(b *testing.T) {
+       stack := NewStackLock(10, 40, func() interface{} {
+               newone := &SimpleStackTest{}
+               return newone
+       })
+       values := []interface{}{}
+       for x := 0; x < 10; x++ {
+               values = append(values, stack.Pop())
+       }
+       if stack.active != 10 {
+               b.Errorf("Failed to match 10 active %v ", stack.active)
+       }
+       value1 := stack.Pop().(*SimpleStackTest)
+       value1.index = stack.active
+       value2 := stack.Pop().(*SimpleStackTest)
+       value2.index = stack.active
+       value3 := stack.Pop().(*SimpleStackTest)
+       value3.index = stack.active
+
+       if !isDifferent(value1, value2, value3) {
+               b.Errorf("Failed to get unique values")
+       }
+
+       if stack.active != 13 {
+               b.Errorf("Failed to match 13 active %v ", stack.active)
+       }
+
+       for _, v := range values {
+               stack.Push(v)
+       }
+       if stack.len != 10 {
+               b.Errorf("Failed to match 10 len %v ", stack.len)
+       }
+       if stack.capacity != 10 {
+               b.Errorf("Failed to capacity 10 len %v ", stack.capacity)
+       }
+
+       stack.Push(value1)
+       stack.Push(value2)
+       stack.Push(value3)
+       if stack.capacity != 13 {
+               b.Errorf("Failed to capacity 13 len %v ", stack.capacity)
+       }
+
+       value1 = stack.Pop().(*SimpleStackTest)
+       value2 = stack.Pop().(*SimpleStackTest)
+       value3 = stack.Pop().(*SimpleStackTest)
+       println(value1, value2, value3)
+       if !isDifferent(value1, value2, value3) {
+               b.Errorf("Failed to get unique values")
+       }
+
+}
+func TestLimits(b *testing.T) {
+       stack := NewStackLock(10, 20, func() interface{} {
+               newone := &SimpleStackTest{}
+               return newone
+       })
+       values := []interface{}{}
+       for x := 0; x < 50; x++ {
+               values = append(values, stack.Pop())
+       }
+       if stack.active != 50 {
+               b.Errorf("Failed to match 50 active %v ", stack.active)
+       }
+       for _, v := range values {
+               stack.Push(v)
+       }
+       if stack.Capacity() != 20 {
+               b.Errorf("Failed to match 20 capcity %v ", stack.Capacity())
+       }
+
+}
+func isDifferent(values ...*SimpleStackTest) bool {
+       if len(values) == 2 {
+               return values[0] != values[1]
+       }
+       for _, v := range values[1:] {
+               if values[0] == v {
+                       return false
+               }
+       }
+       return isDifferent(values[1:]...)
+}
+
+func BenchmarkCreateWrite(b *testing.B) {
+       stack := NewStackLock(0, 40, func() interface{} { return &SimpleStackTest{} })
+       for x := 0; x < b.N; x++ {
+               stack.Push(x)
+       }
+}
+func BenchmarkAllocWrite(b *testing.B) {
+       stack := NewStackLock(b.N, b.N+100, func() interface{} { return &SimpleStackTest{} })
+       for x := 0; x < b.N; x++ {
+               stack.Push(x)
+       }
+}
+func BenchmarkCreate(b *testing.B) {
+       NewStackLock(b.N, b.N+100, func() interface{} { return &SimpleStackTest{} })
+}
+func BenchmarkParrallel(b *testing.B) {
+       stack := NewStackLock(b.N, b.N+100, func() interface{} { return &SimpleStackTest{} })
+       b.RunParallel(func(pb *testing.PB) {
+               for pb.Next() {
+                       for x := 0; x < 50000; x++ {
+                               stack.Push(x)
+                       }
+               }
+       })
+       b.RunParallel(func(pb *testing.PB) {
+               for pb.Next() {
+                       for x := 0; x < 50000; x++ {
+                               stack.Pop()
+                       }
+               }
+       })
+}
diff --git a/src/foundation/api/revel/validation.go b/src/foundation/api/revel/validation.go
new file mode 100644 (file)
index 0000000..49cf3a9
--- /dev/null
@@ -0,0 +1,334 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "fmt"
+       "net/http"
+       "net/url"
+       "regexp"
+       "runtime"
+)
+
+// ValidationError simple struct to store the Message & Key of a validation error
+type ValidationError struct {
+       Message, Key string
+}
+
+// String returns the Message field of the ValidationError struct.
+func (e *ValidationError) String() string {
+       if e == nil {
+               return ""
+       }
+       return e.Message
+}
+
+// Validation context manages data validation and error messages.
+type Validation struct {
+       Errors     []*ValidationError
+       Request    *Request
+       Translator func(locale, message string, args ...interface{}) string
+       keep       bool
+}
+
+// Keep tells revel to set a flash cookie on the client to make the validation
+// errors available for the next request.
+// This is helpful  when redirecting the client after the validation failed.
+// It is good practice to always redirect upon a HTTP POST request. Thus
+// one should use this method when HTTP POST validation failed and redirect
+// the user back to the form.
+func (v *Validation) Keep() {
+       v.keep = true
+}
+
+// Clear *all* ValidationErrors
+func (v *Validation) Clear() {
+       v.Errors = []*ValidationError{}
+}
+
+// HasErrors returns true if there are any (ie > 0) errors. False otherwise.
+func (v *Validation) HasErrors() bool {
+       return len(v.Errors) > 0
+}
+
+// ErrorMap returns the errors mapped by key.
+// If there are multiple validation errors associated with a single key, the
+// first one "wins".  (Typically the first validation will be the more basic).
+func (v *Validation) ErrorMap() map[string]*ValidationError {
+       m := map[string]*ValidationError{}
+       for _, e := range v.Errors {
+               if _, ok := m[e.Key]; !ok {
+                       m[e.Key] = e
+               }
+       }
+       return m
+}
+
+// Error adds an error to the validation context.
+func (v *Validation) Error(message string, args ...interface{}) *ValidationResult {
+       result := v.ValidationResult(false).Message(message, args...)
+       v.Errors = append(v.Errors, result.Error)
+       return result
+}
+
+// Error adds an error to the validation context.
+func (v *Validation) ErrorKey(message string, args ...interface{}) *ValidationResult {
+       result := v.ValidationResult(false).MessageKey(message, args...)
+       v.Errors = append(v.Errors, result.Error)
+       return result
+}
+
+// Error adds an error to the validation context.
+func (v *Validation) ValidationResult(ok bool) *ValidationResult {
+       if ok {
+               return &ValidationResult{Ok: ok}
+       } else {
+               return &ValidationResult{Ok: ok, Error: &ValidationError{}, Locale: v.Request.Locale, Translator: v.Translator}
+       }
+}
+
+// ValidationResult is returned from every validation method.
+// It provides an indication of success, and a pointer to the Error (if any).
+type ValidationResult struct {
+       Error      *ValidationError
+       Ok         bool
+       Locale     string
+       Translator func(locale, message string, args ...interface{}) string
+}
+
+// Key sets the ValidationResult's Error "key" and returns itself for chaining
+func (r *ValidationResult) Key(key string) *ValidationResult {
+       if r.Error != nil {
+               r.Error.Key = key
+       }
+       return r
+}
+
+// Message sets the error message for a ValidationResult. Returns itself to
+// allow chaining.  Allows Sprintf() type calling with multiple parameters
+func (r *ValidationResult) Message(message string, args ...interface{}) *ValidationResult {
+       if r.Error != nil {
+               if len(args) == 0 {
+                       r.Error.Message = message
+               } else {
+                       r.Error.Message = fmt.Sprintf(message, args...)
+               }
+       }
+       return r
+}
+
+// Allow a message key to be passed into the validation result. The Validation has already
+// setup the translator to translate the message key
+func (r *ValidationResult) MessageKey(message string, args ...interface{}) *ValidationResult {
+       if r.Error == nil {
+               return r
+       }
+
+       // If translator found, use that to create the message, otherwise call Message method
+       if r.Translator != nil {
+               r.Error.Message = r.Translator(r.Locale, message, args...)
+       } else {
+               r.Message(message, args...)
+       }
+
+       return r
+}
+
+// Required tests that the argument is non-nil and non-empty (if string or list)
+func (v *Validation) Required(obj interface{}) *ValidationResult {
+       return v.apply(Required{}, obj)
+}
+
+func (v *Validation) Min(n int, min int) *ValidationResult {
+       return v.MinFloat(float64(n), float64(min))
+}
+
+func (v *Validation) MinFloat(n float64, min float64) *ValidationResult {
+       return v.apply(Min{min}, n)
+}
+
+func (v *Validation) Max(n int, max int) *ValidationResult {
+       return v.MaxFloat(float64(n), float64(max))
+}
+
+func (v *Validation) MaxFloat(n float64, max float64) *ValidationResult {
+       return v.apply(Max{max}, n)
+}
+
+func (v *Validation) Range(n, min, max int) *ValidationResult {
+       return v.RangeFloat(float64(n), float64(min), float64(max))
+}
+
+func (v *Validation) RangeFloat(n, min, max float64) *ValidationResult {
+       return v.apply(Range{Min{min}, Max{max}}, n)
+}
+
+func (v *Validation) MinSize(obj interface{}, min int) *ValidationResult {
+       return v.apply(MinSize{min}, obj)
+}
+
+func (v *Validation) MaxSize(obj interface{}, max int) *ValidationResult {
+       return v.apply(MaxSize{max}, obj)
+}
+
+func (v *Validation) Length(obj interface{}, n int) *ValidationResult {
+       return v.apply(Length{n}, obj)
+}
+
+func (v *Validation) Match(str string, regex *regexp.Regexp) *ValidationResult {
+       return v.apply(Match{regex}, str)
+}
+
+func (v *Validation) Email(str string) *ValidationResult {
+       return v.apply(Email{Match{emailPattern}}, str)
+}
+
+func (v *Validation) IPAddr(str string, cktype ...int) *ValidationResult {
+       return v.apply(IPAddr{cktype}, str)
+}
+
+func (v *Validation) MacAddr(str string) *ValidationResult {
+       return v.apply(IPAddr{}, str)
+}
+
+func (v *Validation) Domain(str string) *ValidationResult {
+       return v.apply(Domain{}, str)
+}
+
+func (v *Validation) URL(str string) *ValidationResult {
+       return v.apply(URL{}, str)
+}
+
+func (v *Validation) PureText(str string, m int) *ValidationResult {
+       return v.apply(PureText{m}, str)
+}
+
+func (v *Validation) FilePath(str string, m int) *ValidationResult {
+       return v.apply(FilePath{m}, str)
+}
+
+func (v *Validation) apply(chk Validator, obj interface{}) *ValidationResult {
+       if chk.IsSatisfied(obj) {
+               return v.ValidationResult(true)
+       }
+
+       // Get the default key.
+       var key string
+       if pc, _, line, ok := runtime.Caller(2); ok {
+               f := runtime.FuncForPC(pc)
+               if defaultKeys, ok := DefaultValidationKeys[f.Name()]; ok {
+                       key = defaultKeys[line]
+               }
+       } else {
+               utilLog.Error("Validation: Failed to get Caller information to look up Validation key")
+       }
+
+       // Add the error to the validation context.
+       err := &ValidationError{
+               Message: chk.DefaultMessage(),
+               Key:     key,
+       }
+       v.Errors = append(v.Errors, err)
+
+       // Also return it in the result.
+       vr := v.ValidationResult(false)
+       vr.Error = err
+       return vr
+}
+
+// Check applies a group of validators to a field, in order, and return the
+// ValidationResult from the first one that fails, or the last one that
+// succeeds.
+func (v *Validation) Check(obj interface{}, checks ...Validator) *ValidationResult {
+       var result *ValidationResult
+       for _, check := range checks {
+               result = v.apply(check, obj)
+               if !result.Ok {
+                       return result
+               }
+       }
+       return result
+}
+
+// ValidationFilter revel Filter function to be hooked into the filter chain.
+func ValidationFilter(c *Controller, fc []Filter) {
+       // If json request, we shall assume json response is intended,
+       // as such no validation cookies should be tied response
+       if c.Params != nil && c.Params.JSON != nil {
+               c.Validation = &Validation{Request: c.Request, Translator: MessageFunc}
+               fc[0](c, fc[1:])
+       } else {
+               errors, err := restoreValidationErrors(c.Request)
+               c.Validation = &Validation{
+                       Errors:     errors,
+                       keep:       false,
+                       Request:    c.Request,
+                       Translator: MessageFunc,
+               }
+               hasCookie := (err != http.ErrNoCookie)
+
+               fc[0](c, fc[1:])
+
+               // Add Validation errors to ViewArgs.
+               c.ViewArgs["errors"] = c.Validation.ErrorMap()
+
+               // Store the Validation errors
+               var errorsValue string
+               if c.Validation.keep {
+                       for _, err := range c.Validation.Errors {
+                               if err.Message != "" {
+                                       errorsValue += "\x00" + err.Key + ":" + err.Message + "\x00"
+                               }
+                       }
+               }
+
+               // When there are errors from Validation and Keep() has been called, store the
+               // values in a cookie. If there previously was a cookie but no errors, remove
+               // the cookie.
+               if errorsValue != "" {
+                       c.SetCookie(&http.Cookie{
+                               Name:     CookiePrefix + "_ERRORS",
+                               Value:    url.QueryEscape(errorsValue),
+                               Domain:   CookieDomain,
+                               Path:     "/",
+                               HttpOnly: true,
+                               Secure:   CookieSecure,
+                       })
+               } else if hasCookie {
+                       c.SetCookie(&http.Cookie{
+                               Name:     CookiePrefix + "_ERRORS",
+                               MaxAge:   -1,
+                               Domain:   CookieDomain,
+                               Path:     "/",
+                               HttpOnly: true,
+                               Secure:   CookieSecure,
+                       })
+               }
+       }
+}
+
+// Restore Validation.Errors from a request.
+func restoreValidationErrors(req *Request) ([]*ValidationError, error) {
+       var (
+               err    error
+               cookie ServerCookie
+               errors = make([]*ValidationError, 0, 5)
+       )
+       if cookie, err = req.Cookie(CookiePrefix + "_ERRORS"); err == nil {
+               ParseKeyValueCookie(cookie.GetValue(), func(key, val string) {
+                       errors = append(errors, &ValidationError{
+                               Key:     key,
+                               Message: val,
+                       })
+               })
+       }
+       return errors, err
+}
+
+// DefaultValidationKeys register default validation keys for all calls to Controller.Validation.Func().
+// Map from (package).func => (line => name of first arg to Validation func)
+// E.g. "myapp/controllers.helper" or "myapp/controllers.(*Application).Action"
+// This is set on initialization in the generated main.go file.
+var DefaultValidationKeys map[string]map[int]string
diff --git a/src/foundation/api/revel/validation_test.go b/src/foundation/api/revel/validation_test.go
new file mode 100644 (file)
index 0000000..a3a7ac7
--- /dev/null
@@ -0,0 +1,106 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "net/http"
+       "net/http/httptest"
+       "testing"
+)
+
+// getRecordedCookie returns the recorded cookie from a ResponseRecorder with
+// the given name. It utilizes the cookie reader found in the standard library.
+func getRecordedCookie(recorder *httptest.ResponseRecorder, name string) (*http.Cookie, error) {
+       r := &http.Response{Header: recorder.HeaderMap}
+       for _, cookie := range r.Cookies() {
+               if cookie.Name == name {
+                       return cookie, nil
+               }
+       }
+       return nil, http.ErrNoCookie
+}
+
+// r.Original.URL.String()
+func validationTester(req *Request, fn func(c *Controller)) *httptest.ResponseRecorder {
+       recorder := httptest.NewRecorder()
+       c := NewTestController(recorder, req.In.GetRaw().(*http.Request))
+       c.Request = req
+
+       ValidationFilter(c, []Filter{I18nFilter, func(c *Controller, _ []Filter) {
+               fn(c)
+       }})
+       return recorder
+}
+
+// Test that errors are encoded into the _ERRORS cookie.
+func TestValidationWithError(t *testing.T) {
+       recorder := validationTester(buildEmptyRequest().Request, func(c *Controller) {
+               c.Validation.Required("")
+               if !c.Validation.HasErrors() {
+                       t.Fatal("errors should be present")
+               }
+               c.Validation.Keep()
+       })
+
+       if cookie, err := getRecordedCookie(recorder, "REVEL_ERRORS"); err != nil {
+               t.Fatal(err)
+       } else if cookie.MaxAge < 0 {
+               t.Fatalf("cookie should not expire")
+       }
+}
+
+// Test that no cookie is sent if errors are found, but Keep() is not called.
+func TestValidationNoKeep(t *testing.T) {
+       recorder := validationTester(buildEmptyRequest().Request, func(c *Controller) {
+               c.Validation.Required("")
+               if !c.Validation.HasErrors() {
+                       t.Fatal("errors should not be present")
+               }
+       })
+
+       if _, err := getRecordedCookie(recorder, "REVEL_ERRORS"); err != http.ErrNoCookie {
+               t.Fatal(err)
+       }
+}
+
+// Test that a previously set _ERRORS cookie is deleted if no errors are found.
+func TestValidationNoKeepCookiePreviouslySet(t *testing.T) {
+       req := buildRequestWithCookie("REVEL_ERRORS", "invalid").Request
+       recorder := validationTester(req, func(c *Controller) {
+               c.Validation.Required("success")
+               if c.Validation.HasErrors() {
+                       t.Fatal("errors should not be present")
+               }
+       })
+
+       if cookie, err := getRecordedCookie(recorder, "REVEL_ERRORS"); err != nil {
+               t.Fatal(err)
+       } else if cookie.MaxAge >= 0 {
+               t.Fatalf("cookie should be deleted")
+       }
+}
+
+func TestValidateMessageKey(t *testing.T) {
+       Init("prod", "github.com/revel/revel/testdata", "")
+       loadMessages(testDataPath)
+
+       // Assert that we have the expected number of languages
+       if len(MessageLanguages()) != 2 {
+               t.Fatalf("Expected messages to contain no more or less than 2 languages, instead there are %d languages", len(MessageLanguages()))
+       }
+       req := buildRequestWithAcceptLanguages("nl").Request
+
+       validationTester(req, func(c *Controller) {
+               c.Validation.Required("").MessageKey("greeting")
+               if msg := c.Validation.Errors[0].Message; msg != "Hallo" {
+                       t.Errorf("Failed expected message Hallo got %s", msg)
+               }
+
+               if !c.Validation.HasErrors() {
+                       t.Fatal("errors should not be present")
+               }
+       })
+
+}
diff --git a/src/foundation/api/revel/validators.go b/src/foundation/api/revel/validators.go
new file mode 100644 (file)
index 0000000..31aef42
--- /dev/null
@@ -0,0 +1,633 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "errors"
+       "fmt"
+       "html"
+       "net"
+       "net/url"
+       "reflect"
+       "regexp"
+       "strconv"
+       "strings"
+       "unicode/utf8"
+)
+
+type Validator interface {
+       IsSatisfied(interface{}) bool
+       DefaultMessage() string
+}
+
+type Required struct{}
+
+func ValidRequired() Required {
+       return Required{}
+}
+
+func (r Required) IsSatisfied(obj interface{}) bool {
+       if obj == nil {
+               return false
+       }
+       switch v := reflect.ValueOf(obj); v.Kind() {
+       case reflect.Array, reflect.Slice, reflect.Map, reflect.String, reflect.Chan:
+               if v.Len() == 0 {
+                       return false
+               }
+       case reflect.Ptr:
+               return r.IsSatisfied(reflect.Indirect(v).Interface())
+       }
+       return !reflect.DeepEqual(obj, reflect.Zero(reflect.TypeOf(obj)).Interface())
+}
+
+func (r Required) DefaultMessage() string {
+       return fmt.Sprintln("Required")
+}
+
+type Min struct {
+       Min float64
+}
+
+func ValidMin(min int) Min {
+       return ValidMinFloat(float64(min))
+}
+
+func ValidMinFloat(min float64) Min {
+       return Min{min}
+}
+
+func (m Min) IsSatisfied(obj interface{}) bool {
+       var (
+               num float64
+               ok  bool
+       )
+       switch reflect.TypeOf(obj).Kind() {
+       case reflect.Float64:
+               num, ok = obj.(float64)
+       case reflect.Float32:
+               ok = true
+               num = float64(obj.(float32))
+       case reflect.Int:
+               ok = true
+               num = float64(obj.(int))
+       }
+
+       if ok {
+               return num >= m.Min
+       }
+       return false
+}
+
+func (m Min) DefaultMessage() string {
+       return fmt.Sprintln("Minimum is", m.Min)
+}
+
+type Max struct {
+       Max float64
+}
+
+func ValidMax(max int) Max {
+       return ValidMaxFloat(float64(max))
+}
+
+func ValidMaxFloat(max float64) Max {
+       return Max{max}
+}
+
+func (m Max) IsSatisfied(obj interface{}) bool {
+       var (
+               num float64
+               ok  bool
+       )
+       switch reflect.TypeOf(obj).Kind() {
+       case reflect.Float64:
+               num, ok = obj.(float64)
+       case reflect.Float32:
+               ok = true
+               num = float64(obj.(float32))
+       case reflect.Int:
+               ok = true
+               num = float64(obj.(int))
+       }
+
+       if ok {
+               return num <= m.Max
+       }
+       return false
+}
+
+func (m Max) DefaultMessage() string {
+       return fmt.Sprintln("Maximum is", m.Max)
+}
+
+// Range requires an integer to be within Min, Max inclusive.
+type Range struct {
+       Min
+       Max
+}
+
+func ValidRange(min, max int) Range {
+       return ValidRangeFloat(float64(min), float64(max))
+}
+
+func ValidRangeFloat(min, max float64) Range {
+       return Range{Min{min}, Max{max}}
+}
+
+func (r Range) IsSatisfied(obj interface{}) bool {
+       return r.Min.IsSatisfied(obj) && r.Max.IsSatisfied(obj)
+}
+
+func (r Range) DefaultMessage() string {
+       return fmt.Sprintln("Range is", r.Min.Min, "to", r.Max.Max)
+}
+
+// MinSize requires an array or string to be at least a given length.
+type MinSize struct {
+       Min int
+}
+
+func ValidMinSize(min int) MinSize {
+       return MinSize{min}
+}
+
+func (m MinSize) IsSatisfied(obj interface{}) bool {
+       if str, ok := obj.(string); ok {
+               return utf8.RuneCountInString(str) >= m.Min
+       }
+       v := reflect.ValueOf(obj)
+       if v.Kind() == reflect.Slice {
+               return v.Len() >= m.Min
+       }
+       return false
+}
+
+func (m MinSize) DefaultMessage() string {
+       return fmt.Sprintln("Minimum size is", m.Min)
+}
+
+// MaxSize requires an array or string to be at most a given length.
+type MaxSize struct {
+       Max int
+}
+
+func ValidMaxSize(max int) MaxSize {
+       return MaxSize{max}
+}
+
+func (m MaxSize) IsSatisfied(obj interface{}) bool {
+       if str, ok := obj.(string); ok {
+               return utf8.RuneCountInString(str) <= m.Max
+       }
+       v := reflect.ValueOf(obj)
+       if v.Kind() == reflect.Slice {
+               return v.Len() <= m.Max
+       }
+       return false
+}
+
+func (m MaxSize) DefaultMessage() string {
+       return fmt.Sprintln("Maximum size is", m.Max)
+}
+
+// Length requires an array or string to be exactly a given length.
+type Length struct {
+       N int
+}
+
+func ValidLength(n int) Length {
+       return Length{n}
+}
+
+func (s Length) IsSatisfied(obj interface{}) bool {
+       if str, ok := obj.(string); ok {
+               return utf8.RuneCountInString(str) == s.N
+       }
+       v := reflect.ValueOf(obj)
+       if v.Kind() == reflect.Slice {
+               return v.Len() == s.N
+       }
+       return false
+}
+
+func (s Length) DefaultMessage() string {
+       return fmt.Sprintln("Required length is", s.N)
+}
+
+// Match requires a string to match a given regex.
+type Match struct {
+       Regexp *regexp.Regexp
+}
+
+func ValidMatch(regex *regexp.Regexp) Match {
+       return Match{regex}
+}
+
+func (m Match) IsSatisfied(obj interface{}) bool {
+       str := obj.(string)
+       return m.Regexp.MatchString(str)
+}
+
+func (m Match) DefaultMessage() string {
+       return fmt.Sprintln("Must match", m.Regexp)
+}
+
+var emailPattern = regexp.MustCompile("^[\\w!#$%&'*+/=?^_`{|}~-]+(?:\\.[\\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\\w](?:[\\w-]*[\\w])?\\.)+[a-zA-Z0-9](?:[\\w-]*[\\w])?$")
+
+type Email struct {
+       Match
+}
+
+func ValidEmail() Email {
+       return Email{Match{emailPattern}}
+}
+
+func (e Email) DefaultMessage() string {
+       return fmt.Sprintln("Must be a valid email address")
+}
+
+const (
+       None               = 0
+       IPAny              = 1
+       IPv4               = 32 // IPv4 (32 chars)
+       IPv6               = 39 // IPv6(39 chars)
+       IPv4MappedIPv6     = 45 // IP4-mapped IPv6 (45 chars) , Ex) ::FFFF:129.144.52.38
+       IPv4CIDR           = IPv4 + 3
+       IPv6CIDR           = IPv6 + 3
+       IPv4MappedIPv6CIDR = IPv4MappedIPv6 + 3
+)
+
+// Requires a string(IP Address) to be within IP Pattern type inclusive.
+type IPAddr struct {
+       Vaildtypes []int
+}
+
+// Requires an IP Address string to be exactly a given  validation type (IPv4, IPv6, IPv4MappedIPv6, IPv4CIDR, IPv6CIDR, IPv4MappedIPv6CIDR OR IPAny)
+func ValidIPAddr(cktypes ...int) IPAddr {
+
+       for _, cktype := range cktypes {
+
+               if cktype != IPAny && cktype != IPv4 && cktype != IPv6 && cktype != IPv4MappedIPv6 && cktype != IPv4CIDR && cktype != IPv6CIDR && cktype != IPv4MappedIPv6CIDR {
+                       return IPAddr{Vaildtypes: []int{None}}
+               }
+       }
+
+       return IPAddr{Vaildtypes: cktypes}
+}
+
+func isWithCIDR(str string, l int) bool {
+
+       if str[l-3] == '/' || str[l-2] == '/' {
+
+               cidr_bit := strings.Split(str, "/")
+               if 2 == len(cidr_bit) {
+                       bit, err := strconv.Atoi(cidr_bit[1])
+                       //IPv4 : 0~32, IPv6 : 0 ~ 128
+                       if err == nil && bit >= 0 && bit <= 128 {
+                               return true
+                       }
+               }
+       }
+
+       return false
+}
+
+func getIPType(str string, l int) int {
+
+       if l < 3 { //least 3 chars (::F)
+               return None
+       }
+
+       has_dot := strings.Index(str[2:], ".")
+       has_colon := strings.Index(str[2:], ":")
+
+       switch {
+       case has_dot > -1 && has_colon == -1 && l >= 7 && l <= IPv4CIDR:
+               if isWithCIDR(str, l) == true {
+                       return IPv4CIDR
+               } else {
+                       return IPv4
+               }
+       case has_dot == -1 && has_colon > -1 && l >= 6 && l <= IPv6CIDR:
+               if isWithCIDR(str, l) == true {
+                       return IPv6CIDR
+               } else {
+                       return IPv6
+               }
+
+       case has_dot > -1 && has_colon > -1 && l >= 14 && l <= IPv4MappedIPv6:
+               if isWithCIDR(str, l) == true {
+                       return IPv4MappedIPv6CIDR
+               } else {
+                       return IPv4MappedIPv6
+               }
+       }
+
+       return None
+}
+
+func (i IPAddr) IsSatisfied(obj interface{}) bool {
+
+       if str, ok := obj.(string); ok {
+
+               l := len(str)
+               ret := getIPType(str, l)
+
+               for _, ck := range i.Vaildtypes {
+
+                       if ret != None && (ck == ret || ck == IPAny) {
+
+                               switch ret {
+                               case IPv4, IPv6, IPv4MappedIPv6:
+                                       ip := net.ParseIP(str)
+
+                                       if ip != nil {
+                                               return true
+                                       }
+
+                               case IPv4CIDR, IPv6CIDR, IPv4MappedIPv6CIDR:
+                                       _, _, err := net.ParseCIDR(str)
+                                       if err == nil {
+                                               return true
+                                       }
+                               }
+                       }
+               }
+       }
+
+       return false
+}
+
+func (i IPAddr) DefaultMessage() string {
+       return fmt.Sprintln("Must be a vaild IP address")
+}
+
+// Requires a MAC Address string to be exactly
+type MacAddr struct{}
+
+func ValidMacAddr() MacAddr {
+
+       return MacAddr{}
+}
+
+func (m MacAddr) IsSatisfied(obj interface{}) bool {
+
+       if str, ok := obj.(string); ok {
+               if _, err := net.ParseMAC(str); err == nil {
+                       return true
+               }
+       }
+
+       return false
+}
+
+func (m MacAddr) DefaultMessage() string {
+       return fmt.Sprintln("Must be a vaild MAC address")
+}
+
+var domainPattern = regexp.MustCompile(`^(([a-zA-Z0-9-\p{L}]{1,63}\.)?(xn--)?[a-zA-Z0-9\p{L}]+(-[a-zA-Z0-9\p{L}]+)*\.)+[a-zA-Z\p{L}]{2,63}$`)
+
+// Requires a Domain string to be exactly
+type Domain struct {
+       Regexp *regexp.Regexp
+}
+
+func ValidDomain() Domain {
+       return Domain{domainPattern}
+}
+
+func (d Domain) IsSatisfied(obj interface{}) bool {
+
+       if str, ok := obj.(string); ok {
+
+               l := len(str)
+               //can't exceed 253 chars.
+               if l > 253 {
+                       return false
+               }
+
+               //first and last char must be alphanumeric
+               if str[l-1] == 46 || str[0] == 46 {
+                       return false
+               }
+
+               return domainPattern.MatchString(str)
+       }
+
+       return false
+}
+
+func (d Domain) DefaultMessage() string {
+       return fmt.Sprintln("Must be a vaild domain address")
+}
+
+var urlPattern = regexp.MustCompile(`^((((https?|ftps?|gopher|telnet|nntp)://)|(mailto:|news:))(%[0-9A-Fa-f]{2}|[-()_.!~*';/?:@#&=+$,A-Za-z0-9\p{L}])+)([).!';/?:,][[:blank:]])?$`)
+
+type URL struct {
+       Domain
+}
+
+func ValidURL() URL {
+       return URL{Domain: ValidDomain()}
+}
+
+func (u URL) IsSatisfied(obj interface{}) bool {
+
+       if str, ok := obj.(string); ok {
+
+               // TODO : Required lot of testing
+               return urlPattern.MatchString(str)
+       }
+
+       return false
+}
+
+func (u URL) DefaultMessage() string {
+       return fmt.Sprintln("Must be a vaild URL address")
+}
+
+/*
+NORMAL BenchmarkRegex-8        2000000000               0.24 ns/op
+STRICT BenchmarkLoop-8         2000000000               0.01 ns/op
+*/
+const (
+       NORMAL = 0
+       STRICT = 4
+)
+
+// Requires a string to be without invisible characters
+type PureText struct {
+       Mode int
+}
+
+func ValidPureText(m int) PureText {
+       if m != NORMAL && m != STRICT { // Q:required fatal error
+               m = STRICT
+       }
+       return PureText{m}
+}
+
+func isPureTextStrict(str string) (bool, error) {
+
+       l := len(str)
+
+       for i := 0; i < l; i++ {
+
+               c := str[i]
+
+               // deny : control char (00-31 without 9(TAB) and Single 10(LF),13(CR)
+               if c >= 0 && c <= 31 && c != 9 && c != 10 && c != 13 {
+                       return false, errors.New("detect control character")
+               }
+
+               // deny : control char (DEL)
+               if c == 127 {
+                       return false, errors.New("detect control character (DEL)")
+               }
+
+               //deny : html tag (< ~ >)
+               if c == 60 {
+
+                       ds := 0
+                       for n := i; n < l; n++ {
+
+                               // 60 (<) , 47(/) | 33(!) | 63(?)
+                               if str[n] == 60 && n+1 <= l && (str[n+1] == 47 || str[n+1] == 33 || str[n+1] == 63) {
+                                       ds = 1
+                                       n += 3 //jump to next char
+                               }
+
+                               // 62 (>)
+                               if ds == 1 && str[n] == 62 {
+                                       return false, errors.New("detect tag (<[!|?]~>)")
+                               }
+                       }
+               }
+
+               //deby : html encoded tag (&xxx;)
+               if c == 38 && i+1 <= l && str[i+1] != 35 {
+
+                       max := i + 64
+                       if max > l {
+                               max = l
+                       }
+                       for n := i; n < max; n++ {
+                               if str[n] == 59 {
+                                       return false, errors.New("detect html encoded ta (&XXX;)")
+                               }
+                       }
+               }
+       }
+
+       return true, nil
+}
+
+// Requires a string to match a given html tag elements regex pattern
+// referrer : http://www.w3schools.com/Tags/
+var elementPattern = regexp.MustCompile(`(?im)<(?P<tag>(/*\s*|\?*|\!*)(figcaption|expression|blockquote|plaintext|textarea|progress|optgroup|noscript|noframes|menuitem|frameset|fieldset|!DOCTYPE|datalist|colgroup|behavior|basefont|summary|section|isindex|details|caption|bgsound|article|address|acronym|strong|strike|source|select|script|output|option|object|legend|keygen|ilayer|iframe|header|footer|figure|dialog|center|canvas|button|applet|video|track|title|thead|tfoot|tbody|table|style|small|param|meter|layer|label|input|frame|embed|blink|audio|aside|alert|time|span|samp|ruby|meta|menu|mark|main|link|html|head|form|font|code|cite|body|base|area|abbr|xss|xml|wbr|var|svg|sup|sub|pre|nav|map|kbd|ins|img|div|dir|dfn|del|col|big|bdo|bdi|!--|ul|tt|tr|th|td|rt|rp|ol|li|hr|em|dt|dl|dd|br|u|s|q|p|i|b|a|(h[0-9]+)))([^><]*)([><]*)`)
+
+// Requires a string to match a given urlencoded regex pattern
+var urlencodedPattern = regexp.MustCompile(`(?im)(\%[0-9a-fA-F]{1,})`)
+
+// Requires a string to match a given control characters regex pattern (ASCII : 00-08, 11, 12, 14, 15-31)
+var controlcharPattern = regexp.MustCompile(`(?im)([\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+)`)
+
+func isPureTextNormal(str string) (bool, error) {
+
+       decoded_str := html.UnescapeString(str)
+
+       matched_urlencoded := urlencodedPattern.MatchString(decoded_str)
+       if matched_urlencoded == true {
+               temp_buf, err := url.QueryUnescape(decoded_str)
+               if err == nil {
+                       decoded_str = temp_buf
+               }
+       }
+
+       matched_element := elementPattern.MatchString(decoded_str)
+       if matched_element == true {
+               return false, errors.New("detect html element")
+       }
+
+       matched_cc := controlcharPattern.MatchString(decoded_str)
+       if matched_cc == true {
+               return false, errors.New("detect control character")
+       }
+
+       return true, nil
+}
+
+func (p PureText) IsSatisfied(obj interface{}) bool {
+
+       if str, ok := obj.(string); ok {
+
+               var ret bool
+               switch p.Mode {
+               case STRICT:
+                       ret, _ = isPureTextStrict(str)
+               case NORMAL:
+                       ret, _ = isPureTextStrict(str)
+               }
+               return ret
+       }
+
+       return false
+}
+
+func (p PureText) DefaultMessage() string {
+       return fmt.Sprintln("Must be a vaild Text")
+}
+
+const (
+       ONLY_FILENAME       = 0
+       ALLOW_RELATIVE_PATH = 1
+)
+
+const regexDenyFileNameCharList = `[\x00-\x1f|\x21-\x2c|\x3b-\x40|\x5b-\x5e|\x60|\x7b-\x7f]+`
+const regexDenyFileName = `|\x2e\x2e\x2f+`
+
+var checkAllowRelativePath = regexp.MustCompile(`(?m)(` + regexDenyFileNameCharList + `)`)
+var checkDenyRelativePath = regexp.MustCompile(`(?m)(` + regexDenyFileNameCharList + regexDenyFileName + `)`)
+
+// Requires an string to be sanitary file path
+type FilePath struct {
+       Mode int
+}
+
+func ValidFilePath(m int) FilePath {
+
+       if m != ONLY_FILENAME && m != ALLOW_RELATIVE_PATH {
+               m = ONLY_FILENAME
+       }
+       return FilePath{m}
+}
+
+func (f FilePath) IsSatisfied(obj interface{}) bool {
+
+       if str, ok := obj.(string); ok {
+
+               var ret bool
+               switch f.Mode {
+
+               case ALLOW_RELATIVE_PATH:
+                       ret = checkAllowRelativePath.MatchString(str)
+                       if ret == false {
+                               return true
+                       }
+               default: //ONLY_FILENAME
+                       ret = checkDenyRelativePath.MatchString(str)
+                       if ret == false {
+                               return true
+                       }
+               }
+       }
+
+       return false
+}
+
+func (f FilePath) DefaultMessage() string {
+       return fmt.Sprintln("Must be a unsanitary string")
+}
diff --git a/src/foundation/api/revel/validators_test.go b/src/foundation/api/revel/validators_test.go
new file mode 100644 (file)
index 0000000..9baf177
--- /dev/null
@@ -0,0 +1,640 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel_test
+
+import (
+       "fmt"
+       "github.com/revel/revel"
+       "net"
+       "reflect"
+       "regexp"
+       "strings"
+       "testing"
+       "time"
+)
+
+const (
+       errorsMessage   = "validation for %s should not be satisfied with %s\n"
+       noErrorsMessage = "validation for %s should be satisfied with %s\n"
+)
+
+type Expect struct {
+       input          interface{}
+       expectedResult bool
+       errorMessage   string
+}
+
+func performTests(validator revel.Validator, tests []Expect, t *testing.T) {
+       for _, test := range tests {
+               if validator.IsSatisfied(test.input) != test.expectedResult {
+                       if test.expectedResult {
+                               t.Errorf(noErrorsMessage, reflect.TypeOf(validator), test.errorMessage)
+                       } else {
+                               t.Errorf(errorsMessage, reflect.TypeOf(validator), test.errorMessage)
+                       }
+               }
+       }
+}
+
+func TestRequired(t *testing.T) {
+       tests := []Expect{
+               {nil, false, "nil data"},
+               {"Testing", true, "non-empty string"},
+               {"", false, "empty string"},
+               {true, true, "true boolean"},
+               {false, false, "false boolean"},
+               {1, true, "positive integer"},
+               {-1, true, "negative integer"},
+               {0, false, "0 integer"},
+               {time.Now(), true, "current time"},
+               {time.Time{}, false, "a zero time"},
+               {func() {}, true, "other non-nil data types"},
+               {net.IP(""), false, "empty IP address"},
+       }
+
+       // testing both the struct and the helper method
+       for _, required := range []revel.Required{{}, revel.ValidRequired()} {
+               performTests(required, tests, t)
+       }
+}
+
+func TestMin(t *testing.T) {
+       tests := []Expect{
+               {11, true, "val > min"},
+               {10, true, "val == min"},
+               {9, false, "val < min"},
+               {true, false, "TypeOf(val) != int"},
+       }
+       for _, min := range []revel.Min{{10}, revel.ValidMin(10)} {
+               performTests(min, tests, t)
+       }
+}
+
+func TestMax(t *testing.T) {
+       tests := []Expect{
+               {9, true, "val < max"},
+               {10, true, "val == max"},
+               {11, false, "val > max"},
+               {true, false, "TypeOf(val) != int"},
+       }
+       for _, max := range []revel.Max{{10}, revel.ValidMax(10)} {
+               performTests(max, tests, t)
+       }
+}
+
+func TestRange(t *testing.T) {
+       tests := []Expect{
+               {50, true, "min <= val <= max"},
+               {10, true, "val == min"},
+               {100, true, "val == max"},
+               {9, false, "val < min"},
+               {101, false, "val > max"},
+       }
+
+       goodValidators := []revel.Range{
+               {revel.Min{10}, revel.Max{100}},
+               revel.ValidRange(10, 100),
+       }
+       for _, rangeValidator := range goodValidators {
+               performTests(rangeValidator, tests, t)
+       }
+
+       testsFloat := []Expect{
+               {50, true, "min <= val <= max"},
+               {10.25, true, "val == min"},
+               {100, true, "val == max"},
+               {9, false, "val < min"},
+               {101, false, "val > max"},
+       }
+       goodValidatorsFloat := []revel.Range{
+               {revel.Min{10.25}, revel.Max{100.5}},
+               revel.ValidRangeFloat(10.25, 100.5),
+       }
+       for _, rangeValidator := range goodValidatorsFloat {
+               performTests(rangeValidator, testsFloat, t)
+       }
+
+       tests = []Expect{
+               {10, true, "min == val == max"},
+               {9, false, "val < min && val < max && min == max"},
+               {11, false, "val > min && val > max && min == max"},
+       }
+
+       goodValidators = []revel.Range{
+               {revel.Min{10}, revel.Max{10}},
+               revel.ValidRange(10, 10),
+       }
+       for _, rangeValidator := range goodValidators {
+               performTests(rangeValidator, tests, t)
+       }
+
+       tests = make([]Expect, 7)
+       for i, num := range []int{50, 100, 10, 9, 101, 0, -1} {
+               tests[i] = Expect{
+                       num,
+                       false,
+                       "min > val < max",
+               }
+       }
+       // these are min/max with values swapped, so the min is the high
+       // and max is the low. rangeValidator.IsSatisfied() should ALWAYS
+       // result in false since val can never be greater than min and less
+       // than max when min > max
+       badValidators := []revel.Range{
+               {revel.Min{100}, revel.Max{10}},
+               revel.ValidRange(100, 10),
+       }
+       for _, rangeValidator := range badValidators {
+               performTests(rangeValidator, tests, t)
+       }
+
+       badValidatorsFloat := []revel.Range{
+               {revel.Min{100}, revel.Max{10}},
+               revel.ValidRangeFloat(100, 10),
+       }
+       for _, rangeValidator := range badValidatorsFloat {
+               performTests(rangeValidator, tests, t)
+       }
+}
+
+func TestMinSize(t *testing.T) {
+       greaterThanMessage := "len(val) >= min"
+       tests := []Expect{
+               {"12", true, greaterThanMessage},
+               {"123", true, greaterThanMessage},
+               {[]int{1, 2}, true, greaterThanMessage},
+               {[]int{1, 2, 3}, true, greaterThanMessage},
+               {"", false, "len(val) <= min"},
+               {"手", false, "len(val) <= min"},
+               {[]int{}, false, "len(val) <= min"},
+               {nil, false, "TypeOf(val) != string && TypeOf(val) != slice"},
+       }
+
+       for _, minSize := range []revel.MinSize{{2}, revel.ValidMinSize(2)} {
+               performTests(minSize, tests, t)
+       }
+}
+
+func TestMaxSize(t *testing.T) {
+       lessThanMessage := "len(val) <= max"
+       tests := []Expect{
+               {"", true, lessThanMessage},
+               {"12", true, lessThanMessage},
+               {"ルビー", true, lessThanMessage},
+               {[]int{}, true, lessThanMessage},
+               {[]int{1, 2}, true, lessThanMessage},
+               {[]int{1, 2, 3}, true, lessThanMessage},
+               {"1234", false, "len(val) >= max"},
+               {[]int{1, 2, 3, 4}, false, "len(val) >= max"},
+       }
+       for _, maxSize := range []revel.MaxSize{{3}, revel.ValidMaxSize(3)} {
+               performTests(maxSize, tests, t)
+       }
+}
+
+func TestLength(t *testing.T) {
+       tests := []Expect{
+               {"12", true, "len(val) == length"},
+               {"火箭", true, "len(val) == length"},
+               {[]int{1, 2}, true, "len(val) == length"},
+               {"123", false, "len(val) > length"},
+               {[]int{1, 2, 3}, false, "len(val) > length"},
+               {"1", false, "len(val) < length"},
+               {[]int{1}, false, "len(val) < length"},
+               {nil, false, "TypeOf(val) != string && TypeOf(val) != slice"},
+       }
+       for _, length := range []revel.Length{{2}, revel.ValidLength(2)} {
+               performTests(length, tests, t)
+       }
+}
+
+func TestMatch(t *testing.T) {
+       tests := []Expect{
+               {"bca123", true, `"[abc]{3}\d*" matches "bca123"`},
+               {"bc123", false, `"[abc]{3}\d*" does not match "bc123"`},
+               {"", false, `"[abc]{3}\d*" does not match ""`},
+       }
+       regex := regexp.MustCompile(`[abc]{3}\d*`)
+       for _, match := range []revel.Match{{regex}, revel.ValidMatch(regex)} {
+               performTests(match, tests, t)
+       }
+}
+
+func TestEmail(t *testing.T) {
+       // unicode char included
+       validStartingCharacters := strings.Split("!#$%^&*_+1234567890abcdefghijklmnopqrstuvwxyzñ", "")
+       invalidCharacters := strings.Split(" ()", "")
+
+       definiteInvalidDomains := []string{
+               "",                  // any empty string (x@)
+               ".com",              // only the TLD (x@.com)
+               ".",                 // only the . (x@.)
+               ".*",                // TLD containing symbol (x@.*)
+               "asdf",              // no TLD
+               "a!@#$%^&*()+_.com", // characters which are not ASCII/0-9/dash(-) in a domain
+               "-a.com",            // host starting with any symbol
+               "a-.com",            // host ending with any symbol
+               "aå.com",            // domain containing unicode (however, unicode domains do exist in the state of xn--<POINT>.com e.g. å.com = xn--5ca.com)
+       }
+
+       // Email pattern is not exposed
+       emailPattern := regexp.MustCompile("^[\\w!#$%&'*+/=?^_`{|}~-]+(?:\\.[\\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\\w](?:[\\w-]*[\\w])?\\.)+[a-zA-Z0-9](?:[\\w-]*[\\w])?$")
+       for _, email := range []revel.Email{{revel.Match{emailPattern}}, revel.ValidEmail()} {
+               var currentEmail string
+
+               // test invalid starting chars
+               for _, startingChar := range validStartingCharacters {
+                       currentEmail = fmt.Sprintf("%sñbc+123@do-main.com", startingChar)
+                       if email.IsSatisfied(currentEmail) {
+                               t.Errorf(noErrorsMessage, "starting characters", fmt.Sprintf("email = %s", currentEmail))
+                       }
+
+                       // validation should fail because of multiple @ symbols
+                       currentEmail = fmt.Sprintf("%s@ñbc+123@do-main.com", startingChar)
+                       if email.IsSatisfied(currentEmail) {
+                               t.Errorf(errorsMessage, "starting characters with multiple @ symbols", fmt.Sprintf("email = %s", currentEmail))
+                       }
+
+                       // should fail simply because of the invalid char
+                       for _, invalidChar := range invalidCharacters {
+                               currentEmail = fmt.Sprintf("%sñbc%s+123@do-main.com", startingChar, invalidChar)
+                               if email.IsSatisfied(currentEmail) {
+                                       t.Errorf(errorsMessage, "invalid starting characters", fmt.Sprintf("email = %s", currentEmail))
+                               }
+                       }
+               }
+
+               // test invalid domains
+               for _, invalidDomain := range definiteInvalidDomains {
+                       currentEmail = fmt.Sprintf("a@%s", invalidDomain)
+                       if email.IsSatisfied(currentEmail) {
+                               t.Errorf(errorsMessage, "invalid domain", fmt.Sprintf("email = %s", currentEmail))
+                       }
+               }
+
+               // should always be satisfied
+               if !email.IsSatisfied("t0.est+email123@1abc0-def.com") {
+                       t.Errorf(noErrorsMessage, "guaranteed valid email", fmt.Sprintf("email = %s", "t0.est+email123@1abc0-def.com"))
+               }
+
+               // should never be satisfied (this is redundant given the loops above)
+               if email.IsSatisfied("a@xcom") {
+                       t.Errorf(noErrorsMessage, "guaranteed invalid email", fmt.Sprintf("email = %s", "a@xcom"))
+               }
+               if email.IsSatisfied("a@@x.com") {
+                       t.Errorf(noErrorsMessage, "guaranteed invalid email", fmt.Sprintf("email = %s", "a@@x.com"))
+               }
+       }
+}
+
+func runIPAddrTestfunc(t *testing.T, test_type int, ipaddr_list map[string]bool, msg_fmt string) {
+
+       // generate dataset for test
+       test_ipaddr_list := []Expect{}
+       for ipaddr, expected := range ipaddr_list {
+               test_ipaddr_list = append(test_ipaddr_list, Expect{input: ipaddr, expectedResult: expected, errorMessage: fmt.Sprintf(msg_fmt, ipaddr)})
+       }
+
+       for _, ip_test_list := range []revel.IPAddr{{[]int{test_type}}, revel.ValidIPAddr(test_type)} {
+               performTests(ip_test_list, test_ipaddr_list, t)
+       }
+}
+
+func TestIPAddr(t *testing.T) {
+
+       //IPv4
+       test_ipv4_ipaddrs := map[string]bool{
+               "192.168.1.1":     true,
+               "127.0.0.1":       true,
+               "10.10.90.12":     true,
+               "8.8.8.8":         true,
+               "4.4.4.4":         true,
+               "912.456.123.123": false,
+               "999.999.999.999": false,
+               "192.192.19.999":  false,
+       }
+
+       //IPv4 with CIDR
+       test_ipv4_with_cidr_ipaddrs := map[string]bool{
+               "192.168.1.1/24": true,
+               "127.0.0.1/32":   true,
+               "10.10.90.12/8":  true,
+               "8.8.8.8/1":      true,
+               "4.4.4.4/7":      true,
+               "192.168.1.1/99": false,
+               "127.0.0.1/9999": false,
+               "10.10.90.12/33": false,
+               "8.8.8.8/128":    false,
+               "4.4.4.4/256":    false,
+       }
+
+       //IPv6
+       test_ipv6_ipaddrs := map[string]bool{
+               "2607:f0d0:1002:51::4":                    true,
+               "2607:f0d0:1002:0051:0000:0000:0000:0004": true,
+               "ff05::1:3":                               true,
+               "FE80:0000:0000:0000:0202:B3FF:FE1E:8329": true,
+               "FE80::0202:B3FF:FE1E:8329":               true,
+               "fe80::202:b3ff:fe1e:8329":                true,
+               "fe80:0000:0000:0000:0202:b3ff:fe1e:8329": true,
+               "2001:470:1f09:495::3":                    true,
+               "2001:470:1f1d:275::1":                    true,
+               "2600:9000:5304:200::1":                   true,
+               "2600:9000:5306:d500::1":                  true,
+               "2600:9000:5301:b600::1":                  true,
+               "2600:9000:5303:900::1":                   true,
+               "127:12:12:12:12:12:!2:!2":                false,
+               "127.0.0.1":                               false,
+               "234:23:23:23:23:23:23":                   false,
+       }
+
+       //IPv6 with CIDR
+       test_ipv6_with_cidr_ipaddrs := map[string]bool{
+               "2000::/5":      true,
+               "2000::/15":     true,
+               "2001:db8::/33": true,
+               "2001:db8::/48": true,
+               "fc00::/7":      true,
+       }
+
+       //IPv4-Mapped Embedded IPv6 Address
+       test_ipv4_mapped_ipv6_ipaddrs := map[string]bool{
+               "2001:470:1f09:495::3:217.126.185.215":         true,
+               "2001:470:1f1d:275::1:213.0.69.132":            true,
+               "2600:9000:5304:200::1:205.251.196.2":          true,
+               "2600:9000:5306:d500::1:205.251.198.213":       true,
+               "2600:9000:5301:b600::1:205.251.193.182":       true,
+               "2600:9000:5303:900::1:205.251.195.9":          true,
+               "0:0:0:0:0:FFFF:222.1.41.90":                   true,
+               "::FFFF:222.1.41.90":                           true,
+               "0000:0000:0000:0000:0000:FFFF:12.155.166.101": true,
+               "12.155.166.101":                               false,
+               "12.12/12":                                     false,
+       }
+
+       runIPAddrTestfunc(t, revel.IPv4, test_ipv4_ipaddrs, "invalid (%s) IPv4 address")
+       runIPAddrTestfunc(t, revel.IPv4CIDR, test_ipv4_with_cidr_ipaddrs, "invalid (%s) IPv4 with CIDR address")
+
+       runIPAddrTestfunc(t, revel.IPv6, test_ipv6_ipaddrs, "invalid (%s) IPv6 address")
+       runIPAddrTestfunc(t, revel.IPv6CIDR, test_ipv6_with_cidr_ipaddrs, "invalid (%s) IPv6 with CIDR address")
+       runIPAddrTestfunc(t, revel.IPv4MappedIPv6, test_ipv4_mapped_ipv6_ipaddrs, "invalid (%s) IPv4-Mapped Embedded IPv6 address")
+}
+
+func TestMacAddr(t *testing.T) {
+
+       macaddr_list := map[string]bool{
+               "02:f3:71:eb:9e:4b": true,
+               "02-f3-71-eb-9e-4b": true,
+               "02f3.71eb.9e4b":    true,
+               "87:78:6e:3e:90:40": true,
+               "87-78-6e-3e-90-40": true,
+               "8778.6e3e.9040":    true,
+               "e7:28:b9:57:ab:36": true,
+               "e7-28-b9-57-ab-36": true,
+               "e728.b957.ab36":    true,
+               "eb:f8:2b:d7:e9:62": true,
+               "eb-f8-2b-d7-e9-62": true,
+               "ebf8.2bd7.e962":    true,
+       }
+
+       test_macaddr_list := []Expect{}
+       for macaddr, expected := range macaddr_list {
+               test_macaddr_list = append(test_macaddr_list, Expect{input: macaddr, expectedResult: expected, errorMessage: fmt.Sprintf("invalid (%s) MAC address", macaddr)})
+       }
+
+       for _, mac_test_list := range []revel.MacAddr{{}, revel.ValidMacAddr()} {
+               performTests(mac_test_list, test_macaddr_list, t)
+       }
+}
+
+func TestDomain(t *testing.T) {
+
+       test_domains := map[string]bool{
+               "대한민국.xn-korea.co.kr":           true,
+               "google.com":                    true,
+               "masełkowski.pl":                true,
+               "maselkowski.pl":                true,
+               "m.maselkowski.pl":              true,
+               "www.masełkowski.pl.com":        true,
+               "xn--masekowski-d0b.pl":         true,
+               "中国互联网络信息中心.中国":                 true,
+               "masełkowski.pl.":               false,
+               "中国互联网络信息中心.xn--masekowski-d0b": false,
+               "a.jp":                     true,
+               "a.co":                     true,
+               "a.co.jp":                  true,
+               "a.co.or":                  true,
+               "a.or.kr":                  true,
+               "qwd-qwdqwd.com":           true,
+               "qwd-qwdqwd.co_m":          false,
+               "qwd-qwdqwd.c":             false,
+               "qwd-qwdqwd.-12":           false,
+               "qwd-qwdqwd.1212":          false,
+               "qwd-qwdqwd.org":           true,
+               "qwd-qwdqwd.ac.kr":         true,
+               "qwd-qwdqwd.gov":           true,
+               "chicken.beer":             true,
+               "aa.xyz":                   true,
+               "google.asn.au":            true,
+               "google.com.au":            true,
+               "google.net.au":            true,
+               "google.priv.at":           true,
+               "google.ac.at":             true,
+               "google.gv.at":             true,
+               "google.avocat.fr":         true,
+               "google.geek.nz":           true,
+               "google.gen.nz":            true,
+               "google.kiwi.nz":           true,
+               "google.org.il":            true,
+               "google.net.il":            true,
+               "www.google.edu.au":        true,
+               "www.google.gov.au":        true,
+               "www.google.csiro.au":      true,
+               "www.google.act.au":        true,
+               "www.google.avocat.fr":     true,
+               "www.google.aeroport.fr":   true,
+               "www.google.co.nz":         true,
+               "www.google.geek.nz":       true,
+               "www.google.gen.nz":        true,
+               "www.google.kiwi.nz":       true,
+               "www.google.parliament.nz": true,
+               "www.google.muni.il":       true,
+               "www.google.idf.il":        true,
+       }
+
+       tests := []Expect{}
+
+       for domain, expected := range test_domains {
+               tests = append(tests, Expect{input: domain, expectedResult: expected, errorMessage: fmt.Sprintf("invalid (%s) domain", domain)})
+       }
+
+       for _, domain := range []revel.Domain{{}, revel.ValidDomain()} {
+               performTests(domain, tests, t)
+       }
+}
+
+func TestURL(t *testing.T) {
+
+       test_urls := map[string]bool{
+               "https://www.google.co.kr/url?sa=t&rct=j&q=&esrc=s&source=web":                                      true,
+               "http://stackoverflow.com/questions/27812164/can-i-import-3rd-party-package-into-golang-playground": true,
+               "https://tour.golang.org/welcome/4":                                                                 true,
+               "https://revel.github.io/":                                                                          true,
+               "https://github.com/revel/revel/commit/bd1d083ee4345e919b3bca1e4c42ca682525e395#diff-972a2b2141d27e9d7a8a4149a7e28eef": true,
+               "https://github.com/ndevilla/iniparser/pull/82#issuecomment-261817064":                                                 true,
+               "http://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=0&rsv_idx=1&tn=baidu&wd=golang":                                            true,
+               "http://www.baidu.com/link?url=DrWkM_beo2M5kB5sLYnItKSQ0Ib3oDhKcPprdtLzAWNfFt_VN5oyD3KwnAKT6Xsk":                       true,
+       }
+
+       tests := []Expect{}
+
+       for url, expected := range test_urls {
+               tests = append(tests, Expect{input: url, expectedResult: expected, errorMessage: fmt.Sprintf("invalid (%s) url", url)})
+       }
+
+       for _, url := range []revel.URL{{}, revel.ValidURL()} {
+               performTests(url, tests, t)
+       }
+
+}
+
+func TestPureTextNormal(t *testing.T) {
+
+       test_txts := map[string]bool{
+               `<script ?>qwdpijqwd</script>qd08j123lneqw\t\nqwedojiqwd\rqwdoihjqwd1d[08jaedl;jkqwd\r\nqdolijqdwqwd`:       false,
+               `a\r\nb<script ?>qwdpijqwd</script>qd08j123lneqw\t\nqwedojiqwd\rqwdoihjqwd1d[08jaedl;jkqwd\r\nqdolijqdwqwd`: false,
+               `Foo<script type="text/javascript">alert(1337)</script>Bar`:                                                 false,
+               `Foo<12>Bar`:              true,
+               `Foo<>Bar`:                true,
+               `Foo</br>Bar`:             false,
+               `Foo <!-- Bar --> Baz`:    false,
+               `I <3 Ponies!`:            true,
+               `I &#32; like Golang\t\n`: true,
+               `I &amp; like Golang\t\n`: false,
+               `<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE log4j:configuration SYSTEM "log4j.dtd"> <log4j:configuration debug="true" xmlns:log4j='http://jakarta.apache.org/log4j/'> <appender name="console" class="org.apache.log4j.ConsoleAppender"> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n" /> </layout> </appender> <root> <level value="DEBUG" /> <appender-ref ref="console" /> </root> </log4j:configuration>`: false,
+               `I like Golang\r\n`:       true,
+               `I like Golang\r\na`:      true,
+               "I &#32; like Golang\t\n": true,
+               "I &amp; like Golang\t\n": false,
+               `ハイレゾ対応ウォークマン®、ヘッドホン、スピーカー「Winter Gift Collection ~Presented by JUJU~」をソニーストアにて販売開始`:                                                                      true,
+               `VAIOパーソナルコンピューター type T TZシリーズ 無償点検・修理のお知らせとお詫び(2009年10月15日更新)`:                                                                                          true,
+               `把百度设为主页关于百度About  Baidu百度推广`:                                                                                                                             true,
+               `%E6%8A%8A%E7%99%BE%E5%BA%A6%E8%AE%BE%E4%B8%BA%E4%B8%BB%E9%A1%B5%E5%85%B3%E4%BA%8E%E7%99%BE%E5%BA%A6About++Baidu%E7%99%BE%E5%BA%A6%E6%8E%A8%E5%B9%BF`:     true,
+               `%E6%8A%8A%E7%99%BE%E5%BA%A6%E8%AE%BE%E4%B8%BA%E4%B8%BB%E9%A1%B5%E5%85%B3%E4%BA%8E%E7%99%BE%E5%BA%A6About%20%20Baidu%E7%99%BE%E5%BA%A6%E6%8E%A8%E5%B9%BF`: true,
+               `abcd/>qwdqwdoijhwer/>qwdojiqwdqwd</>qwdoijqwdoiqjd`:                                                                                                      true,
+               `abcd/>qwdqwdoijhwer/>qwdojiqwdqwd</a>qwdoijqwdoiqjd`:                                                                                                     false,
+       }
+
+       tests := []Expect{}
+
+       for txt, expected := range test_txts {
+               tests = append(tests, Expect{input: txt, expectedResult: expected, errorMessage: fmt.Sprintf("invalid (%#v) text", txt)})
+       }
+
+       // normal
+       for _, txt := range []revel.PureText{{revel.NORMAL}, revel.ValidPureText(revel.NORMAL)} {
+               performTests(txt, tests, t)
+       }
+}
+
+func TestPureTextStrict(t *testing.T) {
+
+       test_txts := map[string]bool{
+               `<script ?>qwdpijqwd</script>qd08j123lneqw\t\nqwedojiqwd\rqwdoihjqwd1d[08jaedl;jkqwd\r\nqdolijqdwqwd`:       false,
+               `a\r\nb<script ?>qwdpijqwd</script>qd08j123lneqw\t\nqwedojiqwd\rqwdoihjqwd1d[08jaedl;jkqwd\r\nqdolijqdwqwd`: false,
+               `Foo<script type="text/javascript">alert(1337)</script>Bar`:                                                 false,
+               `Foo<12>Bar`:              true,
+               `Foo<>Bar`:                true,
+               `Foo</br>Bar`:             false,
+               `Foo <!-- Bar --> Baz`:    false,
+               `I <3 Ponies!`:            true,
+               `I &#32; like Golang\t\n`: true,
+               `I &amp; like Golang\t\n`: false,
+               `<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE log4j:configuration SYSTEM "log4j.dtd"> <log4j:configuration debug="true" xmlns:log4j='http://jakarta.apache.org/log4j/'> <ender name="console" class="org.apache.log4j.ConsoleAppender"> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss} %-5p 1}:%L - %m%n" /> </layout> </appender> <root> <level value="DEBUG" /> <appender-ref ref="console" /> </root> </log4j:configuration>`: false,
+               `I like Golang\r\n`:       true,
+               `I like Golang\r\na`:      true,
+               "I &#32; like Golang\t\n": true,
+               "I &amp; like Golang\t\n": false,
+               `ハイレゾ対応ウォークマン®、ヘッドホン、スピーカー「Winter Gift Collection ~Presented by JUJU~」をソニーストアにて販売開始`:                                                                      true,
+               `VAIOパーソナルコンピューター type T TZシリーズ 無償点検・修理のお知らせとお詫び(2009年10月15日更新)`:                                                                                          true,
+               `把百度设为主页关于百度About  Baidu百度推广`:                                                                                                                             true,
+               `%E6%8A%8A%E7%99%BE%E5%BA%A6%E8%AE%BE%E4%B8%BA%E4%B8%BB%E9%A1%B5%E5%85%B3%E4%BA%8E%E7%99%BE%E5%BA%A6About++Baidu%E7%99%BE%E5%BA%A6%E6%8E%A8%E5%B9%BF`:     true,
+               `%E6%8A%8A%E7%99%BE%E5%BA%A6%E8%AE%BE%E4%B8%BA%E4%B8%BB%E9%A1%B5%E5%85%B3%E4%BA%8E%E7%99%BE%E5%BA%A6About%20%20Baidu%E7%99%BE%E5%BA%A6%E6%8E%A8%E5%B9%BF`: true,
+               `abcd/>qwdqwdoijhwer/>qwdojiqwdqwd</>qwdoijqwdoiqjd`:                                                                                                      true,
+               `abcd/>qwdqwdoijhwer/>qwdojiqwdqwd</a>qwdoijqwdoiqjd`:                                                                                                     false,
+       }
+
+       tests := []Expect{}
+
+       for txt, expected := range test_txts {
+               tests = append(tests, Expect{input: txt, expectedResult: expected, errorMessage: fmt.Sprintf("invalid (%#v) text", txt)})
+       }
+
+       // strict
+       for _, txt := range []revel.PureText{{revel.STRICT}, revel.ValidPureText(revel.STRICT)} {
+               performTests(txt, tests, t)
+       }
+}
+
+func TestFilePathOnlyFilePath(t *testing.T) {
+
+       test_filepaths := map[string]bool{
+               "../../qwdqwdqwd/../qwdqwdqwd.txt": false,
+               `../../qwdqwdqwd/..
+                                       /qwdqwdqwd.txt`: false,
+               "\t../../qwdqwdqwd/../qwdqwdqwd.txt": false,
+               `\16../../qwdqwdqwd/../qwdqwdqwd.txt`: false,
+               `../../qwdqwdqwd/..\ 2/qwdqwdqwd.txt`: false,
+               "../../etc/passwd":                 false,
+               "a.txt;rm -rf /":                   false,
+               "sudo rm -rf ../":                  false,
+               "a-1-s-d-v-we-wd_+qwd-qwd-qwd.txt": false,
+               "a-qwdqwd_qwdqwdqwd-123.txt":       true,
+               "a.txt": true,
+               "a-1-e-r-t-_1_21234_d_1234_qwed_1423_.txt": true,
+       }
+
+       tests := []Expect{}
+
+       for filepath, expected := range test_filepaths {
+               tests = append(tests, Expect{input: filepath, expectedResult: expected, errorMessage: fmt.Sprintf("unsanitary (%#v) string", filepath)})
+       }
+
+       // filename without relative path
+       for _, filepath := range []revel.FilePath{{revel.ONLY_FILENAME}, revel.ValidFilePath(revel.ONLY_FILENAME)} {
+               performTests(filepath, tests, t)
+       }
+}
+
+func TestFilePathAllowRelativePath(t *testing.T) {
+
+       test_filepaths := map[string]bool{
+               "../../qwdqwdqwd/../qwdqwdqwd.txt": true,
+               `../../qwdqwdqwd/..
+                                       /qwdqwdqwd.txt`: false,
+               "\t../../qwdqwdqwd/../qwdqwdqwd.txt": false,
+               `\16../../qwdqwdqwd/../qwdqwdqwd.txt`: false,
+               `../../qwdqwdqwd/..\ 2/qwdqwdqwd.txt`: false,
+               "../../etc/passwd":                 true,
+               "a.txt;rm -rf /":                   false,
+               "sudo rm -rf ../":                  true,
+               "a-1-s-d-v-we-wd_+qwd-qwd-qwd.txt": false,
+               "a-qwdqwd_qwdqwdqwd-123.txt":       true,
+               "a.txt": true,
+               "a-1-e-r-t-_1_21234_d_1234_qwed_1423_.txt":                                       true,
+               "/asdasd/asdasdasd/qwdqwd_qwdqwd/12-12/a-1-e-r-t-_1_21234_d_1234_qwed_1423_.txt": true,
+       }
+
+       tests := []Expect{}
+
+       for filepath, expected := range test_filepaths {
+               tests = append(tests, Expect{input: filepath, expectedResult: expected, errorMessage: fmt.Sprintf("unsanitary (%#v) string", filepath)})
+       }
+
+       // filename with relative path
+       for _, filepath := range []revel.FilePath{{revel.ALLOW_RELATIVE_PATH}, revel.ValidFilePath(revel.ALLOW_RELATIVE_PATH)} {
+               performTests(filepath, tests, t)
+       }
+}
diff --git a/src/foundation/api/revel/version.go b/src/foundation/api/revel/version.go
new file mode 100644 (file)
index 0000000..6d666ca
--- /dev/null
@@ -0,0 +1,16 @@
+// Copyright (c) 2012-2018 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+const (
+       // Version current Revel version
+       Version = "0.21.0"
+
+       // BuildDate latest commit/release date
+       BuildDate = "2018-10-30"
+
+       // MinimumGoVersion minimum required Go version for Revel
+       MinimumGoVersion = ">= go1.8"
+)
diff --git a/src/foundation/api/revel/watcher.go b/src/foundation/api/revel/watcher.go
new file mode 100644 (file)
index 0000000..0dfc18c
--- /dev/null
@@ -0,0 +1,299 @@
+// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
+// Revel Framework source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package revel
+
+import (
+       "os"
+       "path/filepath"
+       "strings"
+       "sync"
+
+       "gopkg.in/fsnotify/fsnotify.v1"
+       "time"
+)
+
+// Listener is an interface for receivers of filesystem events.
+type Listener interface {
+       // Refresh is invoked by the watcher on relevant filesystem events.
+       // If the listener returns an error, it is served to the user on the current request.
+       Refresh() *Error
+}
+
+// DiscerningListener allows the receiver to selectively watch files.
+type DiscerningListener interface {
+       Listener
+       WatchDir(info os.FileInfo) bool
+       WatchFile(basename string) bool
+}
+
+// Watcher allows listeners to register to be notified of changes under a given
+// directory.
+type Watcher struct {
+       serial              bool                // true to process events in serial
+       watchers            []*fsnotify.Watcher // Parallel arrays of watcher/listener pairs.
+       listeners           []Listener          // List of listeners for watcher
+       forceRefresh        bool                // True to force the refresh
+       lastError           int                 // The last error found
+       notifyMutex         sync.Mutex          // The mutext to serialize watches
+       refreshTimer        *time.Timer         // The timer to countdown the next refresh
+       timerMutex          *sync.Mutex         // A mutex to prevent concurrent updates
+       refreshChannel      chan *Error         // The error channel to listen to when waiting for a refresh
+       refreshChannelCount int                 // The number of clients listening on the channel
+       refreshTimerMS      time.Duration       // The number of milliseconds between refreshing builds
+}
+
+func NewWatcher() *Watcher {
+       return &Watcher{
+               forceRefresh:        true,
+               lastError:           -1,
+               refreshTimerMS:      time.Duration(Config.IntDefault("watch.rebuild.delay", 10)),
+               timerMutex:          &sync.Mutex{},
+               refreshChannel:      make(chan *Error, 10),
+               refreshChannelCount: 0,
+       }
+}
+
+// Listen registers for events within the given root directories (recursively).
+func (w *Watcher) Listen(listener Listener, roots ...string) {
+       watcher, err := fsnotify.NewWatcher()
+       if err != nil {
+               utilLog.Fatal("Watcher: Failed to create watcher", "error", err)
+       }
+
+       // Replace the unbuffered Event channel with a buffered one.
+       // Otherwise multiple change events only come out one at a time, across
+       // multiple page views.  (There appears no way to "pump" the events out of
+       // the watcher)
+       // This causes a notification when you do a check in go, since you are modifying a buffer in use
+       watcher.Events = make(chan fsnotify.Event, 100)
+       watcher.Errors = make(chan error, 10)
+
+       // Walk through all files / directories under the root, adding each to watcher.
+       for _, p := range roots {
+               // is the directory / file a symlink?
+               f, err := os.Lstat(p)
+               if err == nil && f.Mode()&os.ModeSymlink == os.ModeSymlink {
+                       var realPath string
+                       realPath, err = filepath.EvalSymlinks(p)
+                       if err != nil {
+                               panic(err)
+                       }
+                       p = realPath
+               }
+
+               fi, err := os.Stat(p)
+               if err != nil {
+                       utilLog.Error("Watcher: Failed to stat watched path, code will continue but auto updates will not work", "path", p, "error", err)
+                       continue
+               }
+
+               // If it is a file, watch that specific file.
+               if !fi.IsDir() {
+                       err = watcher.Add(p)
+                       if err != nil {
+                               utilLog.Error("Watcher: Failed to watch, code will continue but auto updates will not work", "path", p, "error", err)
+                       }
+                       continue
+               }
+
+               var watcherWalker func(path string, info os.FileInfo, err error) error
+
+               watcherWalker = func(path string, info os.FileInfo, err error) error {
+                       if err != nil {
+                               utilLog.Error("Watcher: Error walking path:", "error", err)
+                               return nil
+                       }
+
+                       if info.IsDir() {
+                               if dl, ok := listener.(DiscerningListener); ok {
+                                       if !dl.WatchDir(info) {
+                                               return filepath.SkipDir
+                                       }
+                               }
+
+                               err := watcher.Add(path)
+                               if err != nil {
+                                       utilLog.Error("Watcher: Failed to watch this path, code will continue but auto updates will not work", "path", path, "error", err)
+                               }
+                       }
+                       return nil
+               }
+
+               // Else, walk the directory tree.
+               err = Walk(p, watcherWalker)
+               if err != nil {
+                       utilLog.Error("Watcher: Failed to walk directory, code will continue but auto updates will not work", "path", p, "error", err)
+               }
+       }
+
+       if w.eagerRebuildEnabled() {
+               // Create goroutine to notify file changes in real time
+               go w.NotifyWhenUpdated(listener, watcher)
+       }
+
+       w.watchers = append(w.watchers, watcher)
+       w.listeners = append(w.listeners, listener)
+}
+
+// NotifyWhenUpdated notifies the watcher when a file event is received.
+func (w *Watcher) NotifyWhenUpdated(listener Listener, watcher *fsnotify.Watcher) {
+
+       for {
+               select {
+               case ev := <-watcher.Events:
+                       if w.rebuildRequired(ev, listener) {
+                               // Serialize listener.Refresh() calls.
+                               if w.serial {
+                                       // Serialize listener.Refresh() calls.
+                                       w.notifyMutex.Lock()
+
+                                       if err := listener.Refresh(); err != nil {
+                                               utilLog.Error("Watcher: Listener refresh reported error:", "error", err)
+                                       }
+                                       w.notifyMutex.Unlock()
+                               } else {
+                                       // Run refresh in parallel
+                                       go func() {
+                                               w.notifyInProcess(listener)
+                                       }()
+                               }
+                       }
+               case <-watcher.Errors:
+                       continue
+               }
+       }
+}
+
+// Notify causes the watcher to forward any change events to listeners.
+// It returns the first (if any) error returned.
+func (w *Watcher) Notify() *Error {
+       // Serialize Notify() calls.
+       w.notifyMutex.Lock()
+       defer w.notifyMutex.Unlock()
+
+       for i, watcher := range w.watchers {
+               listener := w.listeners[i]
+
+               // Pull all pending events / errors from the watcher.
+               refresh := false
+               for {
+                       select {
+                       case ev := <-watcher.Events:
+                               if w.rebuildRequired(ev, listener) {
+                                       refresh = true
+                               }
+                               continue
+                       case <-watcher.Errors:
+                               continue
+                       default:
+                               // No events left to pull
+                       }
+                       break
+               }
+
+               if w.forceRefresh || refresh || w.lastError == i {
+                       var err *Error
+                       if w.serial {
+                               err = listener.Refresh()
+                       } else {
+                               err = w.notifyInProcess(listener)
+                       }
+
+                       if err != nil {
+                               w.lastError = i
+                               return err
+                       }
+               }
+       }
+
+       w.forceRefresh = false
+       w.lastError = -1
+       return nil
+}
+
+// Build a queue for refresh notifications
+// this will not return until one of the queue completes
+func (w *Watcher) notifyInProcess(listener Listener) (err *Error) {
+       shouldReturn := false
+       // This code block ensures that either a timer is created
+       // or that a process would be added the the h.refreshChannel
+       func() {
+               w.timerMutex.Lock()
+               defer w.timerMutex.Unlock()
+               // If we are in the process of a rebuild, forceRefresh will always be true
+               w.forceRefresh = true
+               if w.refreshTimer != nil {
+                       utilLog.Info("Found existing timer running, resetting")
+                       w.refreshTimer.Reset(time.Millisecond * w.refreshTimerMS)
+                       shouldReturn = true
+                       w.refreshChannelCount++
+               } else {
+                       w.refreshTimer = time.NewTimer(time.Millisecond * w.refreshTimerMS)
+               }
+       }()
+
+       // If another process is already waiting for the timer this one
+       // only needs to return the output from the channel
+       if shouldReturn {
+               return <-w.refreshChannel
+       }
+       utilLog.Info("Waiting for refresh timer to expire")
+       <-w.refreshTimer.C
+       w.timerMutex.Lock()
+
+       // Ensure the queue is properly dispatched even if a panic occurs
+       defer func() {
+               for x := 0; x < w.refreshChannelCount; x++ {
+                       w.refreshChannel <- err
+               }
+               w.refreshChannelCount = 0
+               w.refreshTimer = nil
+               w.timerMutex.Unlock()
+       }()
+
+       err = listener.Refresh()
+       if err != nil {
+               utilLog.Info("Watcher: Recording error last build, setting rebuild on", "error", err)
+       } else {
+               w.lastError = -1
+               w.forceRefresh = false
+       }
+       utilLog.Info("Rebuilt, result", "error", err)
+       return
+}
+
+// If watch.mode is set to eager, the application is rebuilt immediately
+// when a source file is changed.
+// This feature is available only in dev mode.
+func (w *Watcher) eagerRebuildEnabled() bool {
+       return Config.BoolDefault("mode.dev", true) &&
+               Config.BoolDefault("watch", true) &&
+               Config.StringDefault("watch.mode", "normal") == "eager"
+}
+
+func (w *Watcher) rebuildRequired(ev fsnotify.Event, listener Listener) bool {
+       // Ignore changes to dotfiles.
+       if strings.HasPrefix(filepath.Base(ev.Name), ".") {
+               return false
+       }
+
+       if dl, ok := listener.(DiscerningListener); ok {
+               if !dl.WatchFile(ev.Name) || ev.Op&fsnotify.Chmod == fsnotify.Chmod {
+                       return false
+               }
+       }
+       return true
+}
+
+var WatchFilter = func(c *Controller, fc []Filter) {
+       if MainWatcher != nil {
+               err := MainWatcher.Notify()
+               if err != nil {
+                       c.Result = c.RenderError(err)
+                       return
+               }
+       }
+       fc[0](c, fc[1:])
+}