This commit is contained in:
msyu
2024-10-23 11:17:42 +03:00
commit ab5c944862
380 changed files with 55823 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
{
"CFMappings":{
"/Taffy":{
"PHYSICAL":"${PWD}"
}
}
}
+14
View File
@@ -0,0 +1,14 @@
changelog:
exclude:
labels:
- release-ignore
categories:
- title: Breaking Changes ⚠️
labels:
- Semver-Major
- title: New Features 🎉
labels:
- Semver-Minor
- title: Bug Fixes 🐞
labels:
- Semver-Patch
+22
View File
@@ -0,0 +1,22 @@
name: Run tests for all PRs
on:
push:
branches:
- feature/github-actions
jobs:
test:
runs-on: ubuntu-20.04
steps:
- name: Checkout Code
id: git-checkout
uses: actions/checkout@v2
- name: Install dependencies
uses: pixl8/github-action-box-install@v2
- name: Run tests
uses: pixl8/github-action-box-test@v3
with:
output_file: /tests/test-results.txt
+86
View File
@@ -0,0 +1,86 @@
name: Release New Version
# Based on: https://github.blog/2021-12-16-5-automations-every-developer-should-be-running/
on:
workflow_dispatch:
inputs:
version:
description: "Largest semver bump of new version (major / minor / patch)"
required: true
type: choice
options:
- patch
- minor
- major
jobs:
bump-version:
runs-on: ubuntu-latest
steps:
# Check out the content (source branch). Use a deploy key so that
# when we push changes, it will trigger the release workflow
# run that runs on: tag. (Using the GitHub token would
# not run the workflow to prevent infinite recursion.)
- name: Check out source
uses: actions/checkout@v2
with:
ssh-key: ${{ secrets.DEPLOY_KEY }}
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: "16"
- name: Setup Git
run: |
git config user.name 'release-bot'
git config user.email 'adamtuttlecodes@gmail.com'
- name: bump version
run: npm version ${{ github.event.inputs.version }}
- name: get-npm-version
id: package-version
uses: martinbeentjes/npm-get-version-action@master
- name: Update version in api.cfc
run: |
cat core/api.cfc | sed 's/local\._taffy\.version = \"[0-9]\{1,\}\.[0-9]\{1,\}\.[0-9]\{1,\}\"/local._taffy.version = "${{ steps.package-version.outputs.current-version }}"/gi' > core/api.new.cfc
rm -f core/api.cfc
mv core/api.new.cfc core/api.cfc
git add core/api.cfc
git commit --amend --no-edit
- name: Cut a docs file for the new release
run: |
cat docs/@next.md | sed 's/@version@/${{ steps.package-version.outputs.current-version }}/gi' > docs/${{ steps.package-version.outputs.current-version }}.md
git add docs/${{ steps.package-version.outputs.current-version }}.md
cat docs/readme.md | sed 's/\<\!--new_docs_links_here--\>/\<\!--new_docs_links_here--\>\n\n- \[\v${{ steps.package-version.outputs.current-version }}]\(${{ steps.package-version.outputs.current-version }}.md\)/g' > docs/readme-next.md
rm -f docs/readme.md
mv docs/readme-next.md docs/readme.md
git add docs/readme.md
git commit --amend --no-edit
- name: Push latest version
run: git push origin main --follow-tags
- name: Create Release Notes
uses: actions/github-script@v4.0.2
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
await github.request(`POST /repos/${{ github.repository }}/releases`, {
tag_name: "v${{ steps.package-version.outputs.current-version }}",
generate_release_notes: true
});
- name: Tweet-trigger-publish-release
uses: mugi111/tweet-trigger-release@v1.1
with:
consumer_key: ${{ secrets.TWITTER_CONSUMER_API_KEY }}
consumer_secret: ${{ secrets.TWITTER_CONSUMER_API_SECRET }}
access_token_key: ${{ secrets.TWITTER_ACCESS_TOKEN }}
access_token_secret: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}
tweet_body: "🍬 Taffy Version ${{steps.package-version.outputs.current-version}} was just released! https://github.com/atuttle/Taffy/releases/tag/v${{ steps.package-version.outputs.current-version }}"
+19
View File
@@ -0,0 +1,19 @@
name: Require Semver Label
on:
pull_request:
types: [opened, reopened, labeled, unlabeled, ready_for_review, synchronize]
jobs:
check_pr_labels:
runs-on: ubuntu-latest
name: Verify that the PR has a valid label
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Verify PR label action
uses: jesusvasquez333/verify-pr-label-action@v1.4.0
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
valid-labels: 'Semver-Major, Semver-Minor, Semver-Patch'
pull-request-number: '${{ github.event.pull_request.number }}'
+51
View File
@@ -0,0 +1,51 @@
.project
.settings
settings.xml
examples/api/jquery.min.js
WEB-INF
tests/tests/output
examples/api_hoth/hoth/exceptions/exceptions/*.log
examples/api_hoth/hoth/exceptions/incidents/*.log
examples/api_hoth/hoth/logs/*.hoth
examples/api_hoth/hoth/logs/*.log
css/*
node_modules/*
package-lock.json
#testbox
tests/testbox
tests/di1
tests/Hoth
tests/BugLogHQ
### Intellij ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm
## Directory-based project format
.idea/
# if you remove the above rule, at least ignore user-specific stuff:
# .idea/workspace.xml
# .idea/tasks.xml
# and these sensitive or high-churn files:
# .idea/dataSources.ids
# .idea/dataSources.xml
# .idea/sqlDataSources.xml
# .idea/dynamic.xml
## File-based project format
*.ipr
*.iml
*.iws
## Additional for IntelliJ
out/
# generated by mpeltonen/sbt-idea plugin
.idea_modules/
# generated by JIRA plugin
atlassian-ide-plugin.xml
# generated by Crashlytics plugin (for Android Studio and Intellij)
com_crashlytics_export_strings.xml
+28
View File
@@ -0,0 +1,28 @@
language: java
sudo: false
jdk:
- openjdk8
services:
- mysql
env:
global:
- TEST_RUNNER_URI=/tests/tests/run.cfm?reporter=text
- DB_USER=travis
- DB_PASS=
matrix:
- CFENGINE=lucee@4.5
- CFENGINE=lucee@5
- CFENGINE=adobe@10
- CFENGINE=adobe@11
- CFENGINE=adobe@2016
before_install:
- git clone --depth 1 https://github.com/foundeo/cfmatrix.git cfmatrix
- mysql -e 'CREATE DATABASE IF NOT EXISTS buglog;'
install:
- bash ./cfmatrix/install.sh
- mysql buglog < tests/BugLogHQ/install/mysql.sql
before_script:
- cfmatrix/bin/box cfconfig cfmapping list
script:
- bash ./cfmatrix/run.sh
- cfmatrix/bin/box server log
+56
View File
@@ -0,0 +1,56 @@
# Contributing to Taffy
Contributions of all shapes and sizes are welcome, encouraged, and greatly appreciated! Not sure where to start? [Learn how here!](https://makeapullrequest.com)
For all contributions, you'll need a [free GitHub account](https://github.com/join).
## Bug Reports / Feature Requests
Please include all of the following information in your ticket:
- CFML Platform and version (e.g. Adobe ColdFusion 9.0.2, or Lucee 4.5.0)
- Java version (look it up in Lucee/CF Administrator, or do `java -version` at the command line)
- Taffy version (for bugs)
## Documentation
Documentation is in the `/docs` folder.
- When fixing a bug for an existing version, if it necessitates a docs update, be sure to update the correct version's markdown file.
- When adding a new feature, make the appropriate updates to `@next.md`. This file will be used to generate the new version file at release time.
✨ There's no such thing as perfect documentation! It can never be thorough enough, nor ever perfectly organized. If you find something confusing or outdated, please be so kind as to file a bug report for it, if you can't or won't fix it. (Yes, documentation bugs!)
## Code
All new development is done against the `main` branch. When you want to make a change and submit it for the Bleeding Edge Release (BER), do the following:
1. [Fork the project](https://github.com/atuttle/Taffy/fork_select)
1. Clone to your local machine: `git clone https://github.com/YOUR-GITHUB-USERNAME/Taffy.git`
1. Create a topic branch for your changes: `git checkout -b BRANCH_NAME`
1. Make your changes and commit them.
1. Push your changes back to your fork. `git push -u origin BRANCH_NAME`
1. Send a pull request ([Learn how here!](https://makeapullrequest.com))
- Please make sure you select `atuttle/main` as the destination branch
### Styling changes
Taffy uses LessCSS to style the dashboard and documentation. To compile changes to `dash.less`, you'll need to have Node.js/npm installed.
1. From the root of your Taffy clone, run `npm ci` to install the dependencies for compiling LessCSS to CSS.
1. Then run `npm run less`. This will compile the latest `dash.less` and update `dash.css`.
### Tests
_⚠️ Unfortunately our test suite doesn't currently run. We're [working on getting it setup on GitHub Actions](https://github.com/atuttle/Taffy/issues/415) and your help doing so would be greatly appreciated._
If at all possible, please include test cases for anything you add or change. To run the tests, you must have [MxUnit](https://mxunit.org/) installed at `/mxunit` (not just a global mapping, put the folder in your web-root, as there are CSS/JS/etc assets that will be needed).
1. Clone the Taffy repo to `/taffy` in your web root.
1. Point your browser at `http://localhost/taffy/tests/` to initialize the test-harness API that the tests will use
1. Point your browser at `http://localhost/taffy/tests/tests/` to run the test suite.
If you are on vanilla Tomcat or another app server (most Lucee users are), you may find that you need to [add an additional servlet mapping](https://docs.taffy.io/#/3.3.0?id=tomcat-jboss-and-other-app-server-idiosyncrasies) to get the tests to run.
Please [report any errors or failures as bugs](https://github.com/atuttle/Taffy/issues), and be sure to include relevant platform information.
+10
View File
@@ -0,0 +1,10 @@
Unfortunately, to keep things simple, I had to sacrifice a little bit of cleanliness.
To install Taffy, all you need to do is either create a Taffy folder in your web root, or create a /taffy mapping;
and in it, place the /dashboard, /core, and /bonus folders from this archive.
Everything else is for purposes of development, testing, and examples.
Enjoy!
-Adam
https://adamtuttle.codes
+13
View File
@@ -0,0 +1,13 @@
The MIT License (MIT)
Copyright (c) 2011 Adam Tuttle and Contributors
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.
What does that mean?
It means you can use Taffy pretty much any way you like. You can fork it. You can include it in a proprietary product, sell it, and not give us a dime. Pretty much the only thing you can't do is hold us accountable if anything goes wrong.
+68
View File
@@ -0,0 +1,68 @@
# [![Taffy: The REST framework for ColdFusion and Lucee](https://raw.github.com/atuttle/Taffy/main/dashboard/logo-lg.png)](https://taffy.io)
[![Build Status](https://travis-ci.org/atuttle/Taffy.svg?branch=main)](https://travis-ci.org/atuttle/Taffy)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat)](https://makeapullrequest.com)
---
**PROBLEM:** Creating REST APIs with the native functionality in ColdFusion 10+ and Lucee is verbose, complex, and developer-hostile. Or maybe you're still on an older version of ColdFusion.
**SOLUTION:** A framework that focuses on Developer Experience and helps you "fall into a pit of success". **You've come to the right place.** Taffy is low friction, simple to get started, and compatible as far back as ColdFusion 8. CF8 was released in 2007! 😱
---
## What makes Taffy's developer experience better?
- It's terse. How terse? [A functional API can fit into a tweet](https://twitter.com/taffyio/status/327415972581486592). (_BEFORE they doubled the max tweet length!_)
- Smart and secure defaults, easily overridden with metadata in most cases.
- Easy to debug: Error messages are returned as JSON by default.
- Integrated dashboard gives you a direct view into how Taffy has parsed your code.
- Optionally integrates with your favorite IOC libraries like **ColdSpring** and **DI/1**, or use the baked-in IOC.
## Currently Supported Versions
- Taffy 3.3.x+
- Taffy 2.2.x
If you file a bug or ask for support please indicate which version of Taffy you're using. If it's an older release, we usually ask you to upgrade. Officially, we promise to support the current and previous **minor** releases and the last **minor** release of the previous **major** release. To the best of our abilities Taffy follows the versioning guidelines defined in [semver](https://semver.org/).
Supported versions get priority for bug fixes. No promises are made to fix bugs filed against _unsupported_ versions. If your version is supported and your bug is reproducible and isolatable, we'll do everything within our power to address it.
If you're on an unsupported version, upgrade is not an option in your case, and you still have an isolatable and reproducible bug, [contact me][5] to discuss further options.
## Documentation
Primary documentation is available at [docs.taffy.io](https://docs.taffy.io), with a few of the more detailed guides remaining [in the wiki][3] for now.
### You can contribute to the documentation
Contributing documentation changes is as easy as submitting a pull request with modifications to the markdown files in the `/docs` folder.
## Roadmap
Features and bug fixes are coordinated via the [GitHub issues list](https://github.com/atuttle/Taffy/issues).
## Community
The most active place where Taffy users and contributors gather is in the **#taffy** channel of the [CFML Slack][4]. It's a great place to ask for help. We also have [a mailing list][1], but it hasn't been used much since the CFML Slack came around.
## Need serious help?
If your problem is too big or too private to ask for help in a chat room, [I am available to hire for freelance work][5].
## LICENSE
> **The MIT License (MIT)**
>
> Copyright (c) 2011 Adam Tuttle and Contributors
>
> 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.
[1]: https://groups.google.com/forum/#!forum/taffy-users
[3]: https://github.com/atuttle/Taffy/wiki
[4]: https://cfml-slack.herokuapp.com
[5]: https://twitter.com/adamtuttle
@@ -0,0 +1,15 @@
<cfcomponent extends="taffy.core.baseSerializer">
<!--- remove because causing tests to fail
<cfset variables.anythingToXml = application.anythingToXml />
--->
<cffunction
name="getAsXML"
output="false"
taffy:mime="application/xml"
taffy:default="true">
<cfreturn application.anythingToXml.toXml(variables.data) />
</cffunction>
</cfcomponent>
+11
View File
@@ -0,0 +1,11 @@
<cfinterface>
<cffunction name="init" hint="I accept a configuration structure to setup and return myself">
<cfargument name="config" />
<cfargument name="tracker" />
</cffunction>
<cffunction name="saveLog" hint="I log or otherwise notify you of an exception">
<cfargument name="exception" />
</cffunction>
</cfinterface>
+553
View File
@@ -0,0 +1,553 @@
<!---
Copyright 2009 Nathan Mische
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
--->
<cfcomponent displayname="JSONUtil" output="false">
<cfset this.deserialzeJSON = deserializeFromJSON />
<cfset this.serializeJSON = serializeToJSON />
<cfset this.deserialize = deserializeFromJSON />
<cfset this.serialize = serializeToJSON />
<cffunction name="init" output="false">
<cfreturn this />
</cffunction>
<cffunction
name="deserializeFromJSON"
access="public"
returntype="any"
output="false"
hint="Converts a JSON (JavaScript Object Notation) string data representation into CFML data, such as a CFML structure or array.">
<cfargument
name="JSONVar"
type="string"
required="true"
hint="A string that contains a valid JSON construct, or variable that represents one." />
<cfargument
name="strictMapping"
type="boolean"
required="false"
default="true"
hint="A Boolean value that specifies whether to convert the JSON strictly, as follows:
<ul>
<li><code>true:</code> (Default) Convert the JSON string to ColdFusion data types that correspond directly to the JSON data types.</li>
<li><code>false:</code> Determine if the JSON string contains representations of ColdFusion queries, and if so, convert them to queries.</li>
</ul>" />
<!--- DECLARE VARIABLES --->
<cfset var ar = ArrayNew(1) />
<cfset var st = StructNew() />
<cfset var dataType = "" />
<cfset var inQuotes = false />
<cfset var startPos = 1 />
<cfset var nestingLevel = 0 />
<cfset var dataSize = 0 />
<cfset var i = 1 />
<cfset var skipIncrement = false />
<cfset var j = 0 />
<cfset var char = "" />
<cfset var dataStr = "" />
<cfset var structVal = "" />
<cfset var structKey = "" />
<cfset var colonPos = "" />
<cfset var qRows = 0 />
<cfset var qCols = "" />
<cfset var qCol = "" />
<cfset var qData = "" />
<cfset var curCharIndex = "" />
<cfset var curChar = "" />
<cfset var result = "" />
<cfset var unescapeVals = "\\,\"",\/,\b,\t,\n,\f,\r" />
<cfset var unescapeToVals = "\,"",/,#Chr(8)#,#Chr(9)#,#Chr(10)#,#Chr(12)#,#Chr(13)#" />
<cfset var unescapeVals2 = '\,",/,b,t,n,f,r' />
<cfset var unescapetoVals2 = '\,",/,#Chr(8)#,#Chr(9)#,#Chr(10)#,#Chr(12)#,#Chr(13)#' />
<cfset var dJSONString = "" />
<cfset var _data = Trim(arguments.JSONVar) />
<!--- NUMBER --->
<cfif IsNumeric(_data)>
<cfreturn Val(_data) />
<!--- NULL --->
<cfelseif _data EQ "null">
<cfreturn "null" />
<!--- BOOLEAN --->
<cfelseif ListFindNoCase("true,false", _data)>
<cfreturn _data />
<!--- EMPTY STRING --->
<cfelseif _data EQ "''" OR _data EQ '""'>
<cfreturn "" />
<!--- STRING --->
<cfelseif ReFind('^"[^\\"]*(?:\\.[^\\"]*)*"$', _data) EQ 1 OR ReFind("^'[^\\']*(?:\\.[^\\']*)*'$", _data) EQ 1>
<cfset _data = mid(_data, 2, Len(_data)-2) />
<!--- If there are any \b, \t, \n, \f, and \r, do extra processing
(required because ReplaceList() won't work with those) --->
<cfif Find("\b", _data) OR Find("\t", _data) OR Find("\n", _data) OR Find("\f", _data) OR Find("\r", _data)>
<cfset curCharIndex = 0 />
<cfset curChar = ""/>
<cfset dJSONString = ArrayNew(1) />
<cfloop condition="true">
<cfset curCharIndex = curCharIndex + 1 />
<cfif curCharIndex GT len(_data)>
<cfbreak />
<cfelse>
<cfset curChar = mid(_data, curCharIndex, 1) />
<cfif curChar EQ "\">
<cfset curCharIndex = curCharIndex + 1 />
<cfset curChar = mid(_data, curCharIndex,1) />
<cfset pos = listFind(unescapeVals2, curChar) />
<cfif pos>
<cfset ArrayAppend(dJSONString,ListGetAt(unescapetoVals2, pos)) />
<cfelse>
<cfset ArrayAppend(dJSONString,"\" & curChar) />
</cfif>
<cfelse>
<cfset ArrayAppend(dJSONString,curChar) />
</cfif>
</cfif>
</cfloop>
<cfreturn ArrayToList(dJSONString,"") />
<cfelse>
<cfreturn ReplaceList(_data, unescapeVals, unescapeToVals) />
</cfif>
<!--- ARRAY, STRUCT, OR QUERY --->
<cfelseif ( Left(_data, 1) EQ "[" AND Right(_data, 1) EQ "]" )
OR ( Left(_data, 1) EQ "{" AND Right(_data, 1) EQ "}" )>
<!--- Store the data type we're dealing with --->
<cfif Left(_data, 1) EQ "[" AND Right(_data, 1) EQ "]">
<cfset dataType = "array" />
<cfelseif ReFindNoCase('^\{"ROWCOUNT":[0-9]+,"COLUMNS":\[("[^"]+",?)+\],"DATA":\{("[^"]+":\[[^]]*\],?)+\}\}$', _data, 0) EQ 1 AND NOT arguments.strictMapping>
<cfset dataType = "queryByColumns" />
<cfelseif ReFindNoCase('^\{"COLUMNS":\[("[^"]+",?)+\],"DATA":\[(\[[^]]*\],?)+\]\}$', _data, 0) EQ 1 AND NOT arguments.strictMapping>
<cfset dataType = "query" />
<cfelse>
<cfset dataType = "struct" />
</cfif>
<!--- Remove the brackets --->
<cfset _data = Trim( Mid(_data, 2, Len(_data)-2) ) />
<!--- Deal with empty array/struct --->
<cfif Len(_data) EQ 0>
<cfif dataType EQ "array">
<cfreturn ar />
<cfelse>
<cfreturn st />
</cfif>
</cfif>
<!--- Loop through the string characters --->
<cfset dataSize = Len(_data) + 1 />
<cfloop condition="#i# LTE #dataSize#">
<cfset skipIncrement = false />
<!--- Save current character --->
<cfset char = Mid(_data, i, 1) />
<!--- If char is a quote, switch the quote status --->
<cfif char EQ '"'>
<cfset inQuotes = NOT inQuotes />
<!--- If char is escape character, skip the next character --->
<cfelseif char EQ "\" AND inQuotes>
<cfset i = i + 2 />
<cfset skipIncrement = true />
<!--- If char is a comma and is not in quotes, or if end of string, deal with data --->
<cfelseif (char EQ "," AND NOT inQuotes AND nestingLevel EQ 0) OR i EQ Len(_data)+1>
<cfset dataStr = Mid(_data, startPos, i-startPos) />
<!--- If data type is array, append data to the array --->
<cfif dataType EQ "array">
<cfset arrayappend( ar, deserializeFromJSON(dataStr, arguments.strictMapping) ) />
<!--- If data type is struct or query or queryByColumns... --->
<cfelseif dataType EQ "struct" OR dataType EQ "query" OR dataType EQ "queryByColumns">
<cfset dataStr = Mid(_data, startPos, i-startPos) />
<cfset colonPos = Find('":', dataStr) />
<cfif colonPos>
<cfset colonPos = colonPos + 1 />
<cfelse>
<cfset colonPos = Find(":", dataStr) />
</cfif>
<cfset structKey = Trim( Mid(dataStr, 1, colonPos-1) ) />
<!--- If needed, remove quotes from keys --->
<cfif Left(structKey, 1) EQ "'" OR Left(structKey, 1) EQ '"'>
<cfset structKey = Mid( structKey, 2, Len(structKey)-2 ) />
</cfif>
<cfset structVal = Mid( dataStr, colonPos+1, Len(dataStr)-colonPos ) />
<!--- If struct, add to the structure --->
<cfif dataType EQ "struct">
<cfset StructInsert( st, structKey, deserializeFromJSON(structVal, arguments.strictMapping) ) />
<!--- If query, build the query --->
<cfelseif dataType EQ "queryByColumns">
<cfif structKey EQ "rowcount">
<cfset qRows = deserializeFromJSON(structVal, arguments.strictMapping) />
<cfelseif structKey EQ "columns">
<cfset qCols = deserializeFromJSON(structVal, arguments.strictMapping) />
<cfset st = QueryNew(ArrayToList(qCols)) />
<cfif qRows>
<cfset QueryAddRow(st, qRows) />
</cfif>
<cfelseif structKey EQ "data">
<cfset qData = deserializeFromJSON(structVal, arguments.strictMapping) />
<cfset ar = StructKeyArray(qData) />
<cfloop from="1" to="#ArrayLen(ar)#" index="j">
<cfloop from="1" to="#st.recordcount#" index="qRows">
<cfset qCol = ar[j] />
<cfset QuerySetCell(st, qCol, qData[qCol][qRows], qRows) />
</cfloop>
</cfloop>
</cfif>
<cfelseif dataType EQ "query">
<cfif structKey EQ "columns">
<cfset qCols = deserializeFromJSON(structVal, arguments.strictMapping) />
<cfset st = QueryNew(ArrayToList(qCols)) />
<cfelseif structKey EQ "data">
<cfset qData = deserializeFromJSON(structVal, arguments.strictMapping) />
<cfloop from="1" to="#ArrayLen(qData)#" index="qRows">
<cfset QueryAddRow(st) />
<cfloop from="1" to="#ArrayLen(qCols)#" index="j">
<cfset qCol = qCols[j] />
<cfset QuerySetCell(st, qCol, qData[qRows][j], qRows) />
</cfloop>
</cfloop>
</cfif>
</cfif>
</cfif>
<cfset startPos = i + 1 />
<!--- If starting a new array or struct, add to nesting level --->
<cfelseif "{[" CONTAINS char AND NOT inQuotes>
<cfset nestingLevel = nestingLevel + 1 />
<!--- If ending an array or struct, subtract from nesting level --->
<cfelseif "]}" CONTAINS char AND NOT inQuotes>
<cfset nestingLevel = nestingLevel - 1 />
</cfif>
<cfif NOT skipIncrement>
<cfset i = i + 1 />
</cfif>
</cfloop>
<!--- Return appropriate value based on data type --->
<cfif dataType EQ "array">
<cfreturn ar />
<cfelse>
<cfreturn st />
</cfif>
<!--- INVALID JSON --->
<cfelse>
<cfthrow message="JSON parsing failure." />
</cfif>
</cffunction>
<cffunction
name="serializeToJSON"
access="public"
returntype="string"
output="false"
hint="Converts ColdFusion data into a JSON (JavaScript Object Notation) representation of the data.">
<cfargument
name="var"
type="any"
required="true"
hint="A ColdFusion data value or variable that represents one." />
<cfargument
name="serializeQueryByColumns"
type="boolean"
required="false"
default="false"
hint="A Boolean value that specifies how to serialize ColdFusion queries.
<ul>
<li><code>false</code>: (Default) Creates an object with two entries: an array of column names and an array of row arrays. This format is required by the HTML format cfgrid tag.</li>
<li><code>true</code>: Creates an object that corresponds to WDDX query format.</li>
</ul>">
<cfargument
name="strictMapping"
type="boolean"
required="false"
default="false"
hint="A Boolean value that specifies whether to convert the ColdFusion data strictly, as follows:
<ul>
<li><code>false:</code> (Default) Convert the ColdFusion data to a JSON string using ColdFusion data types.</li>
<li><code>true:</code> Convert the ColdFusion data to a JSON string using underlying Java/SQL data types.</li>
</ul>" />
<!--- VARIABLE DECLARATION --->
<cfset var jsonString = "" />
<cfset var tempVal = "" />
<cfset var arKeys = "" />
<cfset var colPos = 1 />
<cfset var md = "" />
<cfset var rowDel = "" />
<cfset var colDel = "" />
<cfset var className = "" />
<cfset var i = 1 />
<cfset var column = "" />
<cfset var datakey = "" />
<cfset var recordcountkey = "" />
<cfset var columnlist = "" />
<cfset var columnlistkey = "" />
<cfset var columnJavaTypes = "" />
<cfset var dJSONString = "" />
<cfset var escapeToVals = "\\,\"",\/,\b,\t,\n,\f,\r" />
<cfset var escapeVals = "\,"",/,#Chr(8)#,#Chr(9)#,#Chr(10)#,#Chr(12)#,#Chr(13)#" />
<cfset var _data = arguments.var />
<cfif arguments.strictMapping>
<!--- GET THE CLASS NAME --->
<cfset className = getClassName(_data) />
</cfif>
<!--- TRY STRICT MAPPING --->
<cfif Len(className) AND CompareNoCase(className,"java.lang.String") eq 0>
<cfreturn '"' & ReplaceList(_data, escapeVals, escapeToVals) & '"' />
<cfelseif Len(className) AND CompareNoCase(className,"java.lang.Boolean") eq 0>
<cfreturn ReplaceList(ToString(_data), 'YES,NO', 'true,false') />
<cfelseif Len(className) AND CompareNoCase(className,"java.lang.Integer") eq 0>
<cfreturn ToString(_data) />
<cfelseif Len(className) AND CompareNoCase(className,"java.lang.Long") eq 0>
<cfreturn ToString(_data) />
<cfelseif Len(className) AND CompareNoCase(className,"java.lang.Float") eq 0>
<cfreturn ToString(_data) />
<cfelseif Len(className) AND CompareNoCase(className,"java.lang.Double") eq 0>
<cfreturn ToString(_data) />
<!--- BINARY --->
<cfelseif IsBinary(_data)>
<cfthrow message="JSON serialization failure: Unable to serialize binary data to JSON." />
<!--- BOOLEAN --->
<cfelseif IsBoolean(_data) AND NOT IsNumeric(_data)>
<cfreturn ReplaceList(YesNoFormat(_data), 'Yes,No', 'true,false') />
<!--- NUMBER --->
<cfelseif IsNumeric(_data)>
<cfif getClassName(_data) eq "java.lang.String">
<cfreturn Val(_data).toString() />
<cfelse>
<cfreturn _data.toString() />
</cfif>
<!--- DATE --->
<cfelseif IsDate(_data)>
<cfreturn '"#DateFormat(_data, "mmmm, dd yyyy")# #TimeFormat(_data, "HH:mm:ss")#"' />
<!--- STRING --->
<cfelseif IsSimpleValue(_data)>
<cfreturn '"' & ReplaceList(_data, escapeVals, escapeToVals) & '"' />
<!--- RAILO XML --->
<cfelseif StructKeyExists(server,"railo") and IsXML(_data)>
<cfreturn '"' & ReplaceList(ToString(_data), escapeVals, escapeToVals) & '"' />
<!--- CUSTOM FUNCTION --->
<cfelseif IsCustomFunction(_data)>
<cfreturn serializeToJSON( GetMetadata(_data), arguments.serializeQueryByColumns, arguments.strictMapping) />
<!--- OBJECT --->
<cfelseif IsObject(_data)>
<cfreturn "{}" />
<!--- ARRAY --->
<cfelseif IsArray(_data)>
<cfset dJSONString = ArrayNew(1) />
<cfloop from="1" to="#ArrayLen(_data)#" index="i">
<cfset tempVal = serializeToJSON( _data[i], arguments.serializeQueryByColumns, arguments.strictMapping ) />
<cfset ArrayAppend(dJSONString,tempVal) />
</cfloop>
<cfreturn "[" & ArrayToList(dJSONString,",") & "]" />
<!--- STRUCT --->
<cfelseif IsStruct(_data)>
<cfset dJSONString = ArrayNew(1) />
<cfset arKeys = StructKeyArray(_data) />
<cfloop from="1" to="#ArrayLen(arKeys)#" index="i">
<cfset tempVal = serializeToJSON(_data[ arKeys[i] ], arguments.serializeQueryByColumns, arguments.strictMapping ) />
<cfset ArrayAppend(dJSONString,'"' & arKeys[i] & '":' & tempVal) />
</cfloop>
<cfreturn "{" & ArrayToList(dJSONString,",") & "}" />
<!--- QUERY --->
<cfelseif IsQuery(_data)>
<cfset dJSONString = ArrayNew(1) />
<!--- Add query meta data --->
<cfset recordcountKey = "ROWCOUNT" />
<cfset columnlistKey = "COLUMNS" />
<cfset columnlist = "" />
<cfset dataKey = "DATA" />
<cfset md = GetMetadata(_data) />
<cfset columnJavaTypes = StructNew() />
<cfloop from="1" to="#ArrayLen(md)#" index="column">
<cfset columnlist = ListAppend(columnlist,UCase(md[column].Name),',') />
<cfif StructKeyExists(md[column],"TypeName")>
<cfset columnJavaTypes[md[column].Name] = getJavaType(md[column].TypeName) />
<cfelse>
<cfset columnJavaTypes[md[column].Name] = "" />
</cfif>
</cfloop>
<cfif arguments.serializeQueryByColumns>
<cfset ArrayAppend(dJSONString,'"#recordcountKey#":' & _data.recordcount) />
<cfset ArrayAppend(dJSONString,',"#columnlistKey#":[' & ListQualify(columnlist, '"') & ']') />
<cfset ArrayAppend(dJSONString,',"#dataKey#":{') />
<cfset colDel = "">
<cfloop list="#columnlist#" delimiters="," index="column">
<cfset ArrayAppend(dJSONString,colDel) />
<cfset ArrayAppend(dJSONString,'"#column#":[') />
<cfset rowDel = "">
<cfloop from="1" to="#_data.recordcount#" index="i">
<cfset ArrayAppend(dJSONString,rowDel) />
<cfif (arguments.strictMapping or StructKeyExists(server,"railo")) AND Len(columnJavaTypes[column])>
<cfset tempVal = serializeToJSON( JavaCast(columnJavaTypes[column],_data[column][i]), arguments.serializeQueryByColumns, arguments.strictMapping ) />
<cfelse>
<cfset tempVal = serializeToJSON( _data[column][i], arguments.serializeQueryByColumns, arguments.strictMapping ) />
</cfif>
<cfset ArrayAppend(dJSONString,tempVal) />
<cfset rowDel = ",">
</cfloop>
<cfset ArrayAppend(dJSONString,']') />
<cfset colDel = ",">
</cfloop>
<cfset ArrayAppend(dJSONString,'}') />
<cfelse>
<cfset ArrayAppend(dJSONString,'"#columnlistKey#":[' & ListQualify(columnlist, '"') & ']') />
<cfset ArrayAppend(dJSONString,',"#dataKey#":[') />
<cfset rowDel = "">
<cfloop from="1" to="#_data.recordcount#" index="i">
<cfset ArrayAppend(dJSONString,rowDel) />
<cfset ArrayAppend(dJSONString,'[') />
<cfset colDel = "">
<cfloop list="#columnlist#" delimiters="," index="column">
<cfset ArrayAppend(dJSONString,colDel) />
<cfif (arguments.strictMapping or StructKeyExists(server,"railo")) AND Len(columnJavaTypes[column])>
<cfset tempVal = serializeToJSON( JavaCast(columnJavaTypes[column],_data[column][i]), arguments.serializeQueryByColumns, arguments.strictMapping ) />
<cfelse>
<cfset tempVal = serializeToJSON( _data[column][i], arguments.serializeQueryByColumns, arguments.strictMapping ) />
</cfif>
<cfset ArrayAppend(dJSONString,tempVal) />
<cfset colDel=","/>
</cfloop>
<cfset ArrayAppend(dJSONString,']') />
<cfset rowDel = "," />
</cfloop>
<cfset ArrayAppend(dJSONString,']') />
</cfif>
<cfreturn "{" & ArrayToList(dJSONString,"") & "}">
<!--- XML --->
<cfelseif IsXML(_data)>
<cfreturn '"' & ReplaceList(ToString(_data), escapeVals, escapeToVals) & '"' />
<!--- UNKNOWN OBJECT TYPE --->
<cfelse>
<cfreturn "{}" />
</cfif>
</cffunction>
<cffunction
name="getJavaType"
access="private"
returntype="string"
output="false"
hint="Maps SQL to Java types. Returns blank string for unhandled SQL types.">
<cfargument
name="sqlType"
type="string"
required="true"
hint="A SQL datatype." />
<cfswitch expression="#arguments.sqlType#">
<cfcase value="bit">
<cfreturn "boolean" />
</cfcase>
<cfcase value="tinyint,smallint,integer">
<cfreturn "int" />
</cfcase>
<cfcase value="bigint">
<cfreturn "long" />
</cfcase>
<cfcase value="real,float">
<cfreturn "float" />
</cfcase>
<cfcase value="double">
<cfreturn "double" />
</cfcase>
<cfcase value="char,varchar,longvarchar">
<cfreturn "string" />
</cfcase>
<cfdefaultcase>
<cfreturn "" />
</cfdefaultcase>
</cfswitch>
</cffunction>
<cffunction
name="getClassName"
access="private"
returntype="string"
output="false"
hint="Returns a variable's underlying java Class name.">
<cfargument
name="data"
type="any"
required="true"
hint="A variable." />
<!--- GET THE CLASS NAME --->
<cftry>
<cfreturn arguments.data.getClass().getName() />
<cfcatch type="any">
<cfreturn "" />
</cfcatch>
</cftry>
</cffunction>
</cfcomponent>
+26
View File
@@ -0,0 +1,26 @@
<cfcomponent extends="taffy.core.baseSerializer">
<!--- <cfset variables.jsonUtil = application.jsonUtil />
<cffunction
name="getAsJson"
output="false"
taffy:mime="application/json"
taffy:default="true">
<cfreturn variables.jsonUtil.serialize(variables.data) />
</cffunction> --->
<cffunction
name="getAsJson"
output="false"
taffy:mime="application/json"
taffy:default="true">
<cfreturn variables.jsonUtil.serialize(variables.data) />
</cffunction>
<cffunction name="setJSONUtil" output="false">
<cfargument name="JSONUtil" required="true" />
<cfset variables.jsonUtil = arguments.JSONUtil />
</cffunction>
</cfcomponent>
+62
View File
@@ -0,0 +1,62 @@
<cfcomponent implements="taffy.bonus.ILogAdapter">
<cffunction name="init">
<cfargument name="config" />
<cfargument name="tracker" />
<!--- <cfdump var="#arguments.config#" abort="true" /> --->
<cfif structKeyExists(arguments, "tracker")>
<!--- used to inject mocking object for testing --->
<cfset variables.blhq = arguments.tracker />
<cfelse>
<cfset var svc = "bugLog.client.bugLogService" />
<cfif structKeyExists( arguments.config, "service" )>
<cfset svc = arguments.config.service />
</cfif>
<cfset variables.blhq = createObject("component", svc) />
<cfset variables.blhq.init(
argumentCollection=arguments.config
) />
</cfif>
<cfparam name="arguments.config.message" default="Exception trapped in API" />
<cfset variables.message = arguments.config.message />
<cfreturn this />
</cffunction>
<cffunction name="saveLog">
<cfargument name="exception" />
<cfset var msg = '' />
<cfif structKeyExists(exception, 'rootcause') && structKeyExists(exception.rootcause, 'cause') && structKeyExists(exception.rootcause.cause, 'message')>
<cfset msg = exception.rootcause.cause.message />
<cfelseif structKeyExists(exception, 'cause') && structKeyExists(exception.cause, 'message')>
<cfset msg = exception.cause.message />
<cfelseif structKeyExists(exception, 'message')>
<cfset msg = exception.message />
<cfelse>
<cfset msg = variables.message />
</cfif>
<!--- You can use addDebugData() in resources to set this value --->
<cfif structKeyExists(request, "debugData") and not structKeyExists(exception, "extraInfo")>
<cfset exception.extraInfo = request.debugData />
</cfif>
<cfset var reqHeaders = getHTTPRequestData().headers />
<cfset var reqBody = getHTTPRequestData().content />
<!--- on input with content-type "application/json" CF seems to expose it as binary data. Here we convert it back to plain text --->
<cfif isBinary(reqBody)>
<cfset reqBody = charsetEncode(reqBody, "UTF-8") />
</cfif>
<cfif isJson(reqBody)>
<cfset reqBody = deserializeJson( reqBody ) />
</cfif>
<cfset variables.blhq.notifyService(msg, arguments.exception, { request_body: reqBody, request_headers: reqHeaders }) />
</cffunction>
</cfcomponent>
+146
View File
@@ -0,0 +1,146 @@
<!---
Adapter Path: taffy.bonus.LogToBugsnag
Configuration Options: (structure)
variables.framework.exceptionLogAdapterConfig = {
apiKey = "c9d60ae4c7e70c4b6c4ebd3e8056d2b8",
appVersion = "1.1.3",
releaseStage = "production"
};
apiKey: The API Key associated with the project
appVersion: The version number of the application which generated the error
releaseStage: The release stage that this error occurred in (e.g "development", "staging" or "production")
--->
<cfcomponent implements="taffy.bonus.ILogAdapter">
<!---
Initializes this logger
--->
<cffunction name="init" hint="I accept a configuration structure to setup and return myself">
<cfargument name="config" />
<cfargument name="tracker" />
<!--- copy settings into adapter instance data --->
<cfset variables.config = structNew() />
<cfset structAppend(variables.config, arguments.config, true) />
<cfreturn this />
</cffunction>
<!---
Logs or notifies the specified exception
--->
<cffunction name="saveLog" hint="I log or otherwise notify you of an exception">
<cfargument name="exception" />
<!--- Define local variables --->
<cfset var payload = structNew() />
<!--- Add Project API key to payload --->
<cfset payload["apiKey"] = variables.config.apiKey />
<!--- Add notifier info to payload --->
<cfset payload["notifier"] = structNew() />
<cfset payload["notifier"]["name"] = "Taffy" />
<cfset payload["notifier"]["version"] = "1.0" />
<cfset payload["notifier"]["url"] = "https://github.com/atuttle/Taffy" />
<!--- Add exception details to payload --->
<cfset payload["events"] = arrayNew(1) />
<cfset payload["events"][1] = structNew() />
<cfset payload["events"][1]["payloadVersion"] = "2" />
<cfset payload["events"][1]["exceptions"] = arrayNew(1) />
<cfset payload["events"][1]["exceptions"][1] = convertException(arguments.exception) />
<!--- Add application details to payload --->
<cfset payload["events"][1]["app"] = structNew() />
<cfset payload["events"][1]["app"]["appVersion"] = variables.config.appVersion />
<cfset payload["events"][1]["app"]["releaseStage"] = variables.config.releaseStage />
<!--- Add some metadata to payload --->
<cfset payload["events"][1]["metaData"] = structNew() />
<cfset payload["events"][1]["metaData"]["request"] = structNew() />
<cfset payload["events"][1]["metaData"]["request"]["remoteAddr"] = cgi.remote_addr />
<cfset payload["events"][1]["metaData"]["request"]["requestMethod"] = cgi.request_method />
<cfset payload["events"][1]["metaData"]["request"]["requestUrl"] = cgi.request_url />
<!--- Send log to Bugsnag --->
<cfhttp url="https://notify.bugsnag.com" method="post">
<cfhttpparam type="header" name="Content-Type" value="application/json" />
<cfhttpparam type="body" value="#serializeJSON(payload)#" />
</cfhttp>
</cffunction>
<!---
Converts the specified exception to be added to the payload
--->
<cffunction name="convertException" access="private" returntype="struct" output="false">
<cfargument name="exception" type="struct" required="true" />
<!--- Define local variables --->
<cfset var root = "" />
<cfset var exceptionElement = structNew() />
<cfset var tagContextElement = "" />
<!--- Get the root exception --->
<cfif structKeyExists(exception, "rootCause")>
<cfset root = arguments.exception.rootCause />
<cfelse>
<cfset root = arguments.exception />
</cfif>
<!--- Add type and message to payload --->
<cfset exceptionElement["errorClass"] = root.type />
<cfset exceptionElement["message"] = root.message />
<!--- Build stack trace --->
<cfset exceptionElement["stacktrace"] = arrayNew(1) />
<cfloop array="#root.TagContext#" index="tagContextElement">
<cfset arrayAppend(exceptionElement["stacktrace"], convertTagContextElement(tagContextElement)) />
</cfloop>
<cfreturn exceptionElement />
</cffunction>
<!---
Converts the specified Tag Context element to be included in the strack trace array
---->
<cffunction name="convertTagContextElement" access="private" returntype="struct" output="false">
<cfargument name="tagContextElement" type="struct" required="true" />
<!--- Define local variables --->
<cfset var stackTraceElement = structNew() />
<!--- Add stack trace data --->
<cfset stackTraceElement["file"] = arguments.tagContextElement.template />
<cfset stackTraceElement["lineNumber"] = arguments.tagContextElement.line />
<cfset stackTraceElement["columnNumber"] = arguments.tagContextElement.column />
<cfset stackTraceElement["method"] = "Unknown function" />
<!--- Add code details --->
<cfif structKeyExists(arguments.tagContextElement, "codePrintPlain")>
<cfset stackTraceElement["code"] = convertCodePrint(arguments.tagContextElement.codePrintPlain) />
</cfif>
<cfreturn stackTraceElement />
</cffunction>
<!---
Converts the specified code details to be included to the strack trace element
--->
<cffunction name="convertCodePrint" access="private" returntype="struct" output="false">
<cfargument name="codePrintText" type="string" required="true" />
<!--- Define local variables --->
<cfset var codeLines = structNew() />
<cfset var split = listToArray(arguments.codePrintText, chr(10)) />
<cfset var line = "" />
<!--- Add code lines --->
<cfloop array="#split#" index="line">
<cfset structInsert(codeLines, listFirst(line, ":"), trim(listRest(line, ":"))) />
</cfloop>
<cfreturn codeLines />
</cffunction>
</cfcomponent>
+13
View File
@@ -0,0 +1,13 @@
<cfcomponent implements="taffy.bonus.ILogAdapter">
<cffunction name="init">
<cfargument name="config" />
<cfargument name="tracker" />
<cfreturn this />
</cffunction>
<cffunction name="saveLog">
<cfargument name="exception" />
</cffunction>
</cfcomponent>
+61
View File
@@ -0,0 +1,61 @@
<cfcomponent implements="taffy.bonus.ILogAdapter">
<cffunction name="init">
<cfargument name="config" />
<cfargument name="tracker" hint="unused" default="" />
<!--- copy settings into adapter instance data --->
<cfset variables.config = {}>
<cfset structAppend( variables.config, arguments.config, true ) />
<cfreturn this />
</cffunction>
<cffunction name="saveLog">
<cfargument name="exception" />
<cfset var local = StructNew() />
<cfset variables.config = removeEmailPrefix(variables.config)>
<!--- to conform to the cfmail attribute name and be backward compatible with emailSubj --->
<cfset variables.config.subject = variables.config.subj>
<cfset local.attributeCollection = variables.config>
<cfmail attributeCollection="#local.attributeCollection#">
<cfif variables.config.type eq "text">
Exception Report
Exception Timestamp: <cfoutput>#dateformat(now(), 'yyyy-mm-dd')# #timeformat(now(), 'HH:MM:SS tt')#</cfoutput>
<cfdump var="#arguments.exception#" format="text" />
<cfif isDefined('request.debugData')>
<cfdump var="#request.debugData#" label="debug data" format="text"/>
</cfif>
<cfelse>
<h2>Exception Report</h2>
<p><strong>Exception Timestamp:</strong> <cfoutput>#dateformat(now(), 'yyyy-mm-dd')# #timeformat(now(), 'HH:MM:SS tt')#</cfoutput></p>
<cfdump var="#arguments.exception#" />
<cfif isDefined('request.debugData')>
<cfdump var="#request.debugData#" label="debug data" />
</cfif>
</cfif>
</cfmail>
</cffunction>
<cffunction name="removeEmailPrefix" output="false" access="private" returntype="struct" hint="removes all email prefix from the config attributes">
<cfargument name="configAttributes" required="true" type="struct" />
<cfset var configAttributeName="" />
<cfset var configAttributeValue = "" />
<cfset var newConfig = {} />
<cfloop collection="#arguments.configAttributes#" item="configAttributeName">
<cfset configAttributeNameWithoutEmailPrefix = replaceNoCase(configAttributeName, "email", "", "one") />
<cfset newConfig[configAttributeNameWithoutEmailPrefix] = arguments.configAttributes[configAttributeName] />
</cfloop>
<cfreturn newConfig />
</cffunction>
</cfcomponent>
+19
View File
@@ -0,0 +1,19 @@
<cfcomponent implements="taffy.bonus.ILogAdapter">
<cffunction name="init">
<cfargument name="config" />
<cfargument name="tracker" default="#createObject("component", "Hoth.HothTracker")#" />
<cfset variables.hothtracker = arguments.tracker />
<cfset variables.hothtracker.init(
createObject("component", arguments.config)
) />
<cfreturn this />
</cffunction>
<cffunction name="saveLog">
<cfargument name="exception" />
<cfset local.result = variables.HothTracker.track(arguments.exception) />
<cfheader name="X-HOTH-LOGGED-EXCEPTION" value="#local.result#" />
</cffunction>
</cfcomponent>
+23
View File
@@ -0,0 +1,23 @@
<cfcomponent implements="taffy.bonus.ILogAdapter">
<cffunction name="init">
<cfargument name="config" />
<cfargument name="tracker" hint="unused" default="" />
<!--- copy settings into adapter instance data --->
<cfset variables.config = {}>
<cfset structAppend( variables.config, arguments.config, true ) />
<cfreturn this />
</cffunction>
<cffunction name="saveLog">
<cfargument name="exception" />
<cfset var logdump = "">
<!--- build dump --->
<cfsavecontent variable="logdump"><cfdump var="#arguments.exception#" format="text"></cfsavecontent>
<!--- write to log --->
<cflog file="#variables.config.logfile#" text="#logdump#" type="Error">
</cffunction>
</cfcomponent>
+20
View File
@@ -0,0 +1,20 @@
<cfcomponent implements="taffy.bonus.ILogAdapter">
<cffunction name="init">
<cfargument name="config" />
<cfargument name="tracker" hint="unused" default="" />
<cfreturn this />
</cffunction>
<cffunction name="saveLog">
<cfargument name="exception" />
<cfcontent type="text/html" />
<cfheader statuscode="500" statustext="Unhandled API Error" />
<cfdump var="#arguments#" />
<cfif isDefined('request.debugData')>
<cfdump var="#request.debugData#" label="debug data" />
</cfif>
<cfabort />
</cffunction>
</cfcomponent>
+64
View File
@@ -0,0 +1,64 @@
{
"name":"Taffy",
"shortDescription":"REST Web Service framework for ColdFusion and Lucee",
"slug":"taffy",
"author":"Adam Tuttle",
"version":"3.3.0",
"homepage":"https://taffy.io/",
"documentation":"https://docs.taffy.io/",
"type":"mvc",
"repository":{
"type":"git",
"url":"https://github.com/atuttle/Taffy.git"
},
"bugs":"https://github.com/atuttle/Taffy/issues",
"license":[
{
"type":"MIT",
"url":"https://raw.githubusercontent.com/atuttle/Taffy/main/LICENSE.TXT"
}
],
"devDependencies":{
"testbox":"^4.2.1+400",
"commandbox-cfconfig":"^1.0.2",
"di1":"git+https://github.com/framework-one/di1.git",
"hoth":"git+https://github.com/aarongreenlee/Hoth.git",
"BugLogHQ":"git+https://github.com/oarevalo/BugLogHQ.git"
},
"installPaths":{
"testbox":"tests/testbox/",
"di1":"tests/di1/",
"Hoth":"tests/Hoth/",
"BugLogHQ":"tests/BugLogHQ/"
},
"engines":[
{
"type":"lucee",
"version":">=4.5.x"
},
{
"type":"adobe",
"version":">=8.0.0"
}
],
"ignore":[
"examples/",
"lib/",
"snippets/",
"tests/",
".cfconfig.json",
".gitignore",
".travis.yml",
"CONTRIBUTING.md",
"build.xml",
"server.json"
],
"testbox": {
"runner": [
{
"default": "http://localhost/runner.cfm"
}
],
"directory": "tests.specs"
}
}
+109
View File
@@ -0,0 +1,109 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<!--
This is an ANT script that runs the MXUnit Integration tests for Taffy
Step 1 in continuous integration!
It assumes that you're running taffy from your web root. Never tried
from an external path + mapping.
-->
<project name="Taffy Integration Tests" basedir="." default="runtests">
<!-- define non-standard tasks -->
<taskdef resource="org/eclipse/jgit/ant/ant-tasks.properties">
<classpath>
<pathelement location="lib/org.eclipse.jgit.ant-1.0.99.0.6-UNOFFICIAL-ROBERTO-RELEASE.jar"/>
<pathelement location="lib/org.eclipse.jgit-2.0.0.201206130900-r.jar"/>
<pathelement location="lib/jsch-0.1.48.jar"/>
</classpath>
</taskdef>
<target name="init">
<!-- //////// DIRECTORY AND CFC PATH SETUP (used in all targets) -->
<!-- what's the directory name of your application? this value will be used throughout this build file;
if you don't want that, just replace the references to ${application.name} with your desired values -->
<property name="application.name" value="taffy" />
<!-- what's the name of the directory where your tests live? Note: this is just the name
of the directory, not the full path-->
<property name="test.dir.name" value="tests/tests" />
<!-- where do your tests live, relative to this build file? test.dir.location will be a
full path to a directory -->
<property name="test.dir.location" location="tests/tests" />
<!-- what is the cfc dot-notation path to that directory, as ColdFusion sees it? -->
<property name="test.cfcpath" value="taffy.tests.tests" />
<!-- //////// MXUNIT ANT TASK SETUP (used in runtests and junitreport targets) -->
<!-- what server and port should your tests run against? -->
<property name="test.server" value="jenkins.local" />
<property name="test.serverport" value="80" />
<!-- what "runner" URL should the tests hit. In this example, you'd be hitting
http://localhost:80/DirectoryNameOfYourProject/test/HttpAntRunner.cfc
Simply copy mxunit/samples/HttpAntRunner.cfc into your test directory! -->
<property name="test.runner" value="/${application.name}/${test.dir.name}/HttpAntRunner.cfc" />
<!-- this is where the xml and html will live for the report generator -->
<property name="test.output" location="${test.dir.name}/output" />
<property name="test.output.xml" location="${test.output}/xml" />
<property name="test.junitoutput" location="${test.output}/html" />
<!-- //////// JAR FILES WE NEED FOR EXTERNAL TASKS -->
<!-- where does the mxunit ant jar file live? it's easiest to copy it out of the mxunit install and put it into your app
You can also put any other ant-relatd jars in this directory; for example, if you want to use svnant, you'll need to put those jars here
-->
<path id="project.classpath">
<fileset dir="lib">
<include name="**/*.jar" />
</fileset>
</path>
<!-- dump the properties -->
<echoproperties prefix="test" />
</target>
<target name="clean" depends="init">
<mkdir dir="${test.output}" />
<mkdir dir="${test.output.xml}" />
<mkdir dir="${test.junitoutput}" />
</target>
<target name="runtests" description="Make output directories and run the MXUnit task" depends="init,clean">
<!-- first, make sure Taffy is initialized -->
<get src="http://jenkins.local/taffy/tests/?dashboard&amp;reload=true" dest="init.result" />
<delete file="init.result" />
<!-- <delete dir="${test.output}" failonerror="false" /> -->
<taskdef name="mxunittask" classname="org.mxunit.ant.MXUnitAntTask" classpathref="project.classpath" />
<mxunittask
server="${test.server}"
port="${test.serverport}"
defaultrunner="${test.runner}"
outputdir="${test.output.xml}"
verbose="true"
failureproperty="testsfailed">
<directory path="${test.dir.location}" recurse="true" packageName="${test.cfcpath}" componentPath="${test.cfcpath}" />
</mxunittask>
<fail if="testsfailed" message="Build fails because tests failed" />
<!-- generate pretty reports -->
<antcall target="junitreport" />
</target>
<target name="junitreport" depends="init" description="Runs the report without running the tests">
<junitreport todir="${test.junitoutput}">
<fileset dir="${test.output.xml}">
<include name="*.xml" />
</fileset>
<report format="frames" todir="${test.junitoutput}" />
</junitreport>
</target>
</project>
File diff suppressed because it is too large Load Diff
+57
View File
@@ -0,0 +1,57 @@
<cfcomponent output="false" hint="a helper class to decode input data">
<!--- the most basic of input handlers, available in all APIs --->
<cffunction name="getFromForm" output="false" taffy:mime="application/x-www-form-urlencoded" hint="get data from form post">
<cfargument name="body" hint="the textual request body" />
<cfset var response = {} />
<cfset var pairs = listToArray(arguments.body, "&") />
<cfset var pair = "" />
<cfset var kv = [] />
<cfset var ix = 0 />
<cfset var k = "" />
<cfset var v = "" />
<cfif not find('=', arguments.body)>
<cfset throwError(400, "You've indicated that you're sending form-encoded data but it doesn't appear to be valid. Aborting request.") />
</cfif>
<cfloop from="1" to="#arrayLen(pairs)#" index="ix">
<cfset pair = pairs[ix] />
<cfset kv = listToArray(pair, "=", true) />
<cfset k = kv[1] />
<cfset v = urlDecode( kv[2] ) />
<cfif structKeyExists( response, k )>
<cfset response[k] = listAppend(response[k], v)>
<cfelse>
<cfset response[k] = v>
</cfif>
</cfloop>
<cfreturn response />
</cffunction>
<!--- ============================ --->
<!--- Helpers --->
<!--- ============================ --->
<cffunction name="throwError" access="private" output="false" returntype="void">
<cfargument name="statusCode" type="numeric" default="500" />
<cfargument name="msg" type="string" required="true" hint="message to return to api consumer" />
<cfargument name="headers" type="struct" required="false" default="#structNew()#" />
<cfcontent reset="true" />
<cfset addHeaders(arguments.headers) />
<cfheader statuscode="#arguments.statusCode#" statustext="#arguments.msg#" />
<cfabort />
</cffunction>
<cffunction name="addHeaders" access="private" output="false" returntype="void">
<cfargument name="headers" type="struct" required="true" />
<cfset var h = '' />
<cfif !structIsEmpty(arguments.headers)>
<cfloop list="#structKeyList(arguments.headers)#" index="h">
<cfheader name="#h#" value="#arguments.headers[h]#" />
</cfloop>
</cfif>
</cffunction>
</cfcomponent>
+199
View File
@@ -0,0 +1,199 @@
<cfcomponent output="false" hint="a helper class to represent easily serializable data">
<cfset variables.data = "" />
<cfset variables.fileName = "" />
<cfset variables.fileMime = "" />
<cfset variables.statusCode = 200 />
<cfset variables.statusText = "OK" />
<cfset variables.miscHeaders = StructNew() />
<cfset variables.deleteFile = false />
<!--- 1= textual, 2= filename, 3= file data --->
<cfset variables.type = 1 />
<cfset variables.types = StructNew() />
<cfset variables.types[1] = "textual" />
<cfset variables.types[2] = "filename" />
<cfset variables.types[3] = "filedata" />
<cfset variables.types[4] = "imagedata" />
<cfset variables.statusTexts = StructNew() />
<cfset variables.statusTexts[100] = "Continue" />
<cfset variables.statusTexts[101] = "Switching Protocols" />
<cfset variables.statusTexts[102] = "Processing" />
<cfset variables.statusTexts[200] = "OK" />
<cfset variables.statusTexts[201] = "Created" />
<cfset variables.statusTexts[202] = "Accepted" />
<cfset variables.statusTexts[203] = "Non-authoritative Information" />
<cfset variables.statusTexts[204] = "No Content" />
<cfset variables.statusTexts[205] = "Reset Content" />
<cfset variables.statusTexts[206] = "Partial Content" />
<cfset variables.statusTexts[207] = "Multi-Status" />
<cfset variables.statusTexts[208] = "Already Reported" />
<cfset variables.statusTexts[226] = "IM Used" />
<cfset variables.statusTexts[300] = "Multiple Choices" />
<cfset variables.statusTexts[301] = "Moved Permanently" />
<cfset variables.statusTexts[302] = "Found" />
<cfset variables.statusTexts[303] = "See Other" />
<cfset variables.statusTexts[304] = "Not Modified" />
<cfset variables.statusTexts[305] = "Use Proxy" />
<cfset variables.statusTexts[307] = "Temporary Redirect" />
<cfset variables.statusTexts[308] = "Permanent Redirect" />
<cfset variables.statusTexts[400] = "Bad Request" />
<cfset variables.statusTexts[401] = "Unauthorized" />
<cfset variables.statusTexts[402] = "Payment Required" />
<cfset variables.statusTexts[403] = "Forbidden" />
<cfset variables.statusTexts[404] = "Not Found" />
<cfset variables.statusTexts[405] = "Method Not Allowed" />
<cfset variables.statusTexts[406] = "Not Acceptable" />
<cfset variables.statusTexts[407] = "Proxy Authentication Required" />
<cfset variables.statusTexts[408] = "Request Timeout" />
<cfset variables.statusTexts[409] = "Conflict" />
<cfset variables.statusTexts[410] = "Gone" />
<cfset variables.statusTexts[411] = "Length Required" />
<cfset variables.statusTexts[412] = "Precondition Failed" />
<cfset variables.statusTexts[413] = "Payload Too Large" />
<cfset variables.statusTexts[414] = "Request-URI Too Long" />
<cfset variables.statusTexts[415] = "Unsupported Media Type" />
<cfset variables.statusTexts[416] = "Requested Range Not Satisfiable" />
<cfset variables.statusTexts[417] = "Expectation Failed" />
<cfset variables.statusTexts[418] = "I'm a teapot" />
<cfset variables.statusTexts[421] = "Misdirected Request" />
<cfset variables.statusTexts[422] = "Unprocessable Entity" />
<cfset variables.statusTexts[423] = "Locked" />
<cfset variables.statusTexts[424] = "Failed Dependency" />
<cfset variables.statusTexts[426] = "Upgrade Required" />
<cfset variables.statusTexts[428] = "Precondition Required" />
<cfset variables.statusTexts[429] = "Too Many Requests" />
<cfset variables.statusTexts[431] = "Request Header Fields Too Large" />
<cfset variables.statusTexts[444] = "Connection Closed Without Response" />
<cfset variables.statusTexts[451] = "Unavailable For Legal Reasons" />
<cfset variables.statusTexts[499] = "Client Closed Request" />
<cfset variables.statusTexts[500] = "Internal Server Error" />
<cfset variables.statusTexts[501] = "Not Implemented" />
<cfset variables.statusTexts[502] = "Bad Gateway" />
<cfset variables.statusTexts[503] = "Service Unavailable" />
<cfset variables.statusTexts[504] = "Gateway Timeout" />
<cfset variables.statusTexts[505] = "HTTP Version Not Supported" />
<cfset variables.statusTexts[506] = "Variant Also Negotiates" />
<cfset variables.statusTexts[507] = "Insufficient Storage" />
<cfset variables.statusTexts[508] = "Loop Detected" />
<cfset variables.statusTexts[510] = "Not Extended" />
<cfset variables.statusTexts[511] = "Network Authentication Required" />
<cfset variables.statusTexts[599] = "Network Connect Timeout Error" />
<cffunction name="getType" acces="public" output="false">
<cfreturn variables.types[variables.type] />
</cffunction>
<cffunction name="setData" access="public" output="false" hint="setter for the data to be returned">
<cfargument name="data" required="true" hint="the simple or complex data that you want to return to the api consumer" />
<cfset variables.type = 1 />
<cfset variables.data = arguments.data />
<cfreturn this />
</cffunction>
<cffunction name="getData" access="public" output="false" hint="mostly for testability, returns the native data embedded in the representation instance">
<cfreturn variables.data />
</cffunction>
<cffunction name="noData" access="public" output="false" hint="returns empty representation instance">
<cfif application._taffy.settings.noDataSends204NoContent>
<cfreturn this.noContent() />
<cfelse>
<cfreturn this />
</cfif>
</cffunction>
<cffunction name="noContent" access="public" output="false" hint="returns empty representation instance">
<!--- According to issue #365 https://github.com/atuttle/Taffy/issues/365
noContent() returns with HTTP status code 204 and Content-Type as text/plain (omitting this header is difficult and maybe not recommanded)
noData() is kept 'as is' for backward compatibility with existing implementations
--->
<cfreturn this.withStatus(204).withHeaders({"Content-Type"="text/plain"}) />
</cffunction>
<cffunction name="setFileName" access="public" output="false" hint="Pass in a file-name (fully qualified, e.g. c:\temp\img.jpg) to have Taffy stream this file back to the client">
<cfargument name="file" type="string" required="true" />
<cfset variables.type = 2 />
<cfset variables.fileName = arguments.file />
<cfreturn this />
</cffunction>
<cffunction name="getFileName" access="public" output="false">
<cfreturn variables.fileName />
</cffunction>
<cffunction name="setFileData" access="public" output="false" hint="Pass in file data (eg a generated PDF object) - NOT a Filename! - to have Taffy stream the content back to the client">
<cfargument name="data" required="true" />
<cfset variables.type = 3 />
<cfset variables.fileData = arguments.data />
<cfreturn this />
</cffunction>
<cffunction name="getFileData" access="public" output="false">
<cfreturn variables.fileData />
</cffunction>
<cffunction name="setImageData" access="public" output="false" hint="Pass in image data (eg a generated image object) - NOT a Filename! - to have Taffy stream the content back to the client">
<cfargument name="data" required="true" />
<cfset variables.type = 4 />
<cfif not isBinary(arguments.data)>
<cfset arguments.data = toBinary(toBase64(arguments.data)) />
</cfif>
<cfset variables.fileData = arguments.data />
<cfreturn this />
</cffunction>
<cffunction name="getImageData" access="public" output="false">
<cfreturn getFileData() />
</cffunction>
<cffunction name="withMime" access="public" output="false" hint="Use this method in conjunction with streamFile and streamBinary in your resources to set the mime type of the file being returned. Ex: return streamFile('kittens/cuteness.jpg').withMime('image/jpeg');">
<cfargument name="mime" type="string" required="true" />
<cfset variables.fileMime = arguments.mime />
<cfreturn this />
</cffunction>
<cffunction name="getFileMime" access="public" output="false">
<cfreturn variables.fileMime />
</cffunction>
<cffunction name="withStatus" access="public" output="false" hint="used to set the http response code for the response">
<cfargument name="statusCode" type="numeric" required="true" hint="eg 200" />
<cfargument name="statusText" type="string" required="false" default="" />
<cfset variables.statusCode = arguments.statusCode />
<cfif len(arguments.statusText)>
<cfset variables.statusText = arguments.statusText />
<cfelseif StructKeyExists(variables.statusTexts, arguments.statusCode)>
<cfset variables.statusText = variables.statusTexts[arguments.statusCode] />
</cfif>
<cfreturn this />
</cffunction>
<cffunction name="getStatus" access="public" output="false" returnType="numeric">
<cfreturn variables.statusCode />
</cffunction>
<cffunction name="getStatusText" access="public" output="false" returnType="string">
<cfreturn variables.statusText />
</cffunction>
<cffunction name="withHeaders" access="public" output="false" hint="used to set custom headers for the response">
<cfargument name="headerStruct" type="struct" required="true" />
<cfset variables.miscHeaders = arguments.headerStruct />
<cfreturn this />
</cffunction>
<cffunction name="getHeaders" access="public" output="false" returntype="Struct">
<cfreturn variables.miscHeaders />
</cffunction>
<cffunction name="andDelete" access="public" output="false" hint="used to delete the streamed file">
<cfargument name="doDeleteFile" type="boolean" required="true" />
<cfset variables.deleteFile = arguments.doDeleteFile />
<cfreturn this />
</cffunction>
<cffunction name="getDeleteFile" access="public" output="false" returntype="boolean">
<cfreturn variables.deleteFile />
</cffunction>
</cfcomponent>
+202
View File
@@ -0,0 +1,202 @@
<cfcomponent output="false">
<cfscript>
//bean cache
this.beans = structNew();
this.transients = structNew();
//functionality
</cfscript>
<cffunction name="init" output="false">
<cfargument name="externalBeanFactory">
<cfscript>
if (structKeyExists(arguments, "externalBeanFactory")) {
this.externalBeanFactory = arguments.externalBeanFactory;
}
return this;
</cfscript>
</cffunction>
<cfscript>
// Proxy to beanExists to provide similar interface to ColdSpring
function containsBean(beanName){
return beanExists(arguments.beanName);
}
function transientExists(beanName){
return structKeyExists(this.transients, arguments.beanName);
}
function getBean(beanName){
var b = 0;
var meta = 0;
if (beanExists(arguments.beanName, false, false)){
return this.beans[arguments.beanName];
}else if (transientExists(arguments.beanName)){
b = createObject('component', this.transients[arguments.beanName]);
meta = getMetadata(b);
_recurse_ResolveDependencies(b, meta);
return b;
}else if (externalBeanExists(arguments.beanName)){
return this.externalBeanFactory.getBean(arguments.beanName);
}else{
throwError(message="Bean name '#arguments.beanName#' not found.", type="Taffy.Factory.BeanNotFound");
}
}
function getBeanList(){
var combined = structKeyList(this.beans);
var trans = structKeyList(this.transients);
if (len(combined) and len(trans)){
combined = combined & ",";
}
combined = combined & trans;
return combined;
}
</cfscript>
<cffunction name="beanExists" output="false">
<cfargument required="true" name="beanName">
<cfargument name="includeTransients" default="true">
<cfargument name="includeExternal" default="false">
<cfscript>
return structKeyExists(this.beans, arguments.beanName) or (arguments.includeTransients and transientExists(arguments.beanName)) or
(arguments.includeExternal and externalBeanExists(arguments.beanName));
</cfscript>
</cffunction>
<cffunction name="externalBeanExists" access="private" output="false" returnType="boolean">
<cfargument required="true" name="beanName">
<cfscript>
return structKeyExists(this, "externalBeanFactory") and this.externalBeanFactory.containsBean(arguments.beanName);
</cfscript>
</cffunction>
<cffunction name="loadBeansFromPath" access="public" output="false" returnType="void">
<cfargument name="beanPath" type="string" required="true" hint="Absolute path to folder containing beans" />
<cfargument name="resourcesPath" type="string" default="resources" />
<cfargument name="resourcesBasePath" type="string" default="" />
<cfargument name="isFullReload" type="boolean" default="false" />
<cfargument name="taffyRef" type="any" required="false" default="#structNew()#" />
<cfset var local = StructNew() />
<!--- cache all of the beans --->
<cfif isFullReload>
<cfset this.beans = structNew() />
<cfset arguments.taffyRef.status.skippedResources = arrayNew(1) /> <!--- empty out the array on factory reloads --->
<cfset arguments.taffyRef.beanList = "" />
</cfif>
<!--- if the folder doesn't exist, do nothing --->
<cfif not directoryExists(arguments.beanPath)>
<cfreturn />
</cfif>
<!--- get list of beans to load --->
<cfdirectory action="list" directory="#arguments.beanPath#" filter="*.cfc" name="local.beanQuery" recurse="true" />
<cfloop query="local.beanQuery">
<cfset local.beanName = filePathToBeanName(local.beanQuery.directory, local.beanquery.name, arguments.resourcesBasePath) />
<cfset local.beanPath = filePathToBeanPath(local.beanQuery.directory, local.beanquery.name, arguments.resourcesPath, arguments.resourcesBasePath) />
<cftry>
<cfset local.objBean = createObject("component", local.beanPath) />
<cfif isInstanceOf(local.objBean, "taffy.core.baseSerializer")>
<cfset this.transients[local.beanName] = local.beanPath />
<cfelse>
<cfset this.beans[local.beanName] = local.objBean />
</cfif>
<cfcatch>
<!--- skip cfc's with errors, but save info about them for display in the dashboard --->
<cfset local.err = structNew() />
<cfset local.err.resource = local.beanName />
<cfset local.err.exception = cfcatch />
<cfset arrayAppend(arguments.taffyRef.status.skippedResources, local.err) />
</cfcatch>
</cftry>
</cfloop>
<!--- resolve dependencies --->
<cfloop list="#structKeyList(this.beans)#" index="local.b">
<cfset local.bean = this.beans[local.b] />
<cfset local.beanMeta = getMetadata(local.bean) />
<cfset _recurse_ResolveDependencies(local.bean, local.beanMeta) />
</cfloop>
</cffunction>
<cffunction name="filePathToBeanPath" access="private">
<cfargument name="path" />
<cfargument name="filename" />
<cfargument name="resourcesPath" />
<cfargument name="resourcesBasePath" />
<cfset var beanPath = "" />
<cfif len(resourcesBasePath) eq 0>
<cfset arguments.resourcesBasePath = "!@$%^&*()" />
</cfif>
<cfset beanPath =
resourcesPath
&
"."
&
replaceList(
replace(path, resourcesBasePath, ""),
"/,\",
".,."
)
&
"."
&
replace(
filename,
".cfc",
""
)
/>
<cfset beanPath = replace(beanPath, "..", ".", "ALL") />
<cfif left(beanPath, 1) eq ".">
<cfset beanPath = right(beanPath, len(beanPath)-1) />
</cfif>
<cfreturn beanPath />
</cffunction>
<cffunction name="filePathToBeanName" access="private">
<cfargument name="path" />
<cfargument name="filename" />
<cfargument name="basepath" />
<cfif len(basepath) eq 0>
<cfset arguments.basePath = "!@$%^&*()" />
</cfif>
<cfreturn
replaceList(
replace(path, basepath, ""),
"/,\",
","
)
& replace(
filename,
".cfc",
""
)
/>
</cffunction>
<cffunction name="_recurse_ResolveDependencies" access="private">
<cfargument name="bean" required="true" />
<cfargument name="metaData" type="struct" required="true" />
<cfset var local = structNew() />
<cfif structKeyExists(arguments.metaData, "functions") and isArray(arguments.metaData.functions)>
<cfloop from="1" to="#arrayLen(arguments.metaData.functions)#" index="local.f">
<cfset local.fname = arguments.metaData.functions[local.f].name />
<cfif len(local.fname) gt 3>
<cfset local.propName = right(local.fname, len(local.fname)-3) />
<cfif left(local.fname, 3) eq "set" and beanExists(local.propName, true, true)>
<cfset evaluate("arguments.bean.#local.fname#(getBean('#local.propName#'))") />
</cfif>
</cfif>
</cfloop>
</cfif>
<cfif structKeyExists(arguments.metaData, "properties") and isArray(arguments.metaData.properties)>
<cfloop from="1" to="#arrayLen(arguments.metaData.properties)#" index="local.p">
<cfset local.propName = arguments.metaData.properties[local.p].name />
<cfif beanExists(local.propName, true, true)>
<cfset arguments.bean[local.propName] = getBean(local.propName) />
</cfif>
</cfloop>
</cfif>
<cfif structKeyExists(arguments.metaData, "extends") and isStruct(arguments.metaData.extends)>
<cfset _recurse_ResolveDependencies(arguments.bean, arguments.metaData.extends) />
</cfif>
</cffunction>
<!--- proxy function for CF8 compatibility --->
<cffunction name="throwError">
<cfthrow attributecollection="#arguments#" />
</cffunction>
</cfcomponent>
+21
View File
@@ -0,0 +1,21 @@
<cfcomponent extends="taffy.core.baseDeserializer">
<cffunction name="getFromJson" output="false" taffy:mime="application/json,text/json" hint="get data from json">
<cfargument name="body" hint="the textual request body" />
<cfset var data = 0 />
<cfset var response = {} />
<cfif not isJson(arguments.body)>
<cfset throwError(msg="Input JSON is not well formed", statusCode="400") />
</cfif>
<cfset data = deserializeJSON(arguments.body) />
<cfif not isStruct(data)>
<cfset response['_body'] = data />
<cfelse>
<cfset response = data />
</cfif>
<cfreturn response />
</cffunction>
</cfcomponent>
+12
View File
@@ -0,0 +1,12 @@
<cfcomponent extends="taffy.core.baseSerializer" output="false" hint="Serializer that uses CFML server's json serialization functionality to return json data">
<cffunction
name="getAsJson"
output="false"
taffy:mime="application/json;text/json"
taffy:default="true"
hint="serializes data as JSON">
<cfreturn rereplace(replace(serializeJSON(variables.data), chr(2), '', 'ALL'), '"\\u0002', '"', 'ALL') />
</cffunction>
</cfcomponent>
+141
View File
@@ -0,0 +1,141 @@
<cfcomponent hint="base class for taffy REST components">
<cffunction name="forceString">
<cfargument name="data" required="true" hint="the data that is being forced to serialize as a string" />
<cfreturn chr(2) & arguments.data />
</cffunction>
<cfset variables.encode = structNew() />
<cfset variables.encode.string = forceString />
<!--- helper functions --->
<cffunction name="representationOf" access="public" output="false" hint="returns an object capable of serializing the data in a variety of formats">
<cfargument name="data" required="true" hint="any simple or complex data that should be returned for the request" />
<cfreturn getRepInstance().setData(arguments.data) />
</cffunction>
<cffunction name="rep" access="public" output="false" hint="alias for representationOf">
<cfargument name="data" required="true" />
<cfreturn representationOf(arguments.data) />
</cffunction>
<cffunction name="noData" access="private" output="false" hint="use this function to return only headers to the consumer, no data">
<cfreturn getRepInstance().noData() />
</cffunction>
<cffunction name="noContent" access="private" output="false" hint="use this function to return only headers to the consumer, no data">
<cfreturn getRepInstance().noContent() />
</cffunction>
<cffunction name="streamFile" access="private" output="false" hint="Use this function to specify a file name (eg c:\tmp\kitten.jpg) to be streamed to the client. When you use this method it is *required* that you also use .withMime() to specify the mime type.">
<cfargument name="fileName" required="true" hint="fully qualified file path (eg c:\tmp\kitten.jpg)" />
<cfreturn getRepInstance().setFileName(arguments.fileName) />
</cffunction>
<cffunction name="streamBinary" access="private" output="false" hint="Use this function to stream binary data, like a generated PDF object, to the client. When you use this method it is *required* that you also use .withMime() to specify the mime type.">
<cfargument name="binaryData" required="true" hint="binary file data (eg a PDF object) that you want to return to the client" />
<cfreturn getRepInstance().setFileData(arguments.binaryData) />
</cffunction>
<cffunction name="streamImage" access="private" output="false" hint="Use this function to stream binary data, like a generated PDF object, to the client. When you use this method it is *required* that you also use .withMime() to specify the mime type.">
<cfargument name="binaryData" required="true" hint="binary file data (eg a PDF object or image data) that you want to return to the client" />
<cfreturn getRepInstance().setImageData(arguments.binaryData) />
</cffunction>
<cffunction name="saveLog">
<cfargument name="exception" />
<cfset logger = createObject("component", application._taffy.settings.exceptionLogAdapter).init(
application._taffy.settings.exceptionLogAdapterConfig
) />
<cfset logger.saveLog(exception) />
</cffunction>
<cffunction name="queryToArray" access="private" returntype="array" output="false">
<cfargument name="q" type="query" required="yes" />
<cfargument name="cb" type="any" required="no" />
<cfscript>
var local = {};
if (structKeyExists(server, "railo") or structKeyExists(server, "lucee")) {
local.Columns = listToArray(arguments.q.getColumnList(false));
}
else {
local.Columns = arguments.q.getMetaData().getColumnLabels();
}
local.QueryArray = ArrayNew(1);
for (local.RowIndex = 1; local.RowIndex <= arguments.q.RecordCount; local.RowIndex++){
local.Row = {};
local.numCols = ArrayLen( local.Columns );
for (local.ColumnIndex = 1; local.ColumnIndex <= local.numCols; local.ColumnIndex++){
local.ColumnName = local.Columns[ local.ColumnIndex ];
if( local.ColumnName NEQ "" ) {
local.Row[ local.ColumnName ] = arguments.q[ local.ColumnName ][ local.RowIndex ];
}
}
if ( structKeyExists( arguments, "cb" ) ) {
local.Row = cb( local.Row );
}
ArrayAppend( local.QueryArray, local.Row );
}
return( local.QueryArray );
</cfscript>
</cffunction>
<cffunction name="queryToStruct" access="private" returntype="struct" output="false">
<cfargument name="q" type="query" required="yes" />
<cfargument name="cb" type="any" required="no" />
<cfset var local = {} />
<cfif q.recordcount gt 1>
<cfthrow message="Unable to convert query resultset with more than one record to a simple struct, use queryToArray() instead" />
</cfif>
<cfscript>
if (structKeyExists(server, "railo") or structKeyExists(server, "lucee")) {
local.Columns = listToArray(arguments.q.getColumnList(false));
}
else {
local.Columns = arguments.q.getMetaData().getColumnLabels();
}
local.QueryStruct = {};
local.numCols = ArrayLen( local.Columns );
for (local.ColumnIndex = 1; local.ColumnIndex <= local.numCols; local.ColumnIndex++){
local.ColumnName = local.Columns[ local.ColumnIndex ];
if( local.ColumnName NEQ "" ) {
if ( structKeyExists( arguments, "cb" ) ) {
local.QueryStruct[ local.ColumnName ] = cb( local.ColumnName, arguments.q[ local.ColumnName ][1] );
} else {
local.QueryStruct[ local.ColumnName ] = arguments.q[ local.ColumnName ][1];
}
}
}
return( local.QueryStruct );
</cfscript>
</cffunction>
<!---
function that gets the representation class instance
-- if the argument is blank, we use the default from taffy settings
-- if the argument is a beanName, the bean is returned from the factory;
-- otherwise it is assumed to be a cfc path and that cfc instance is returned
--->
<cffunction name="getRepInstance" access="private" output="false">
<cfargument name="repClass" type="string" default="" />
<cfif repClass eq "">
<!--- recursion not the most efficient path here, but it's damn readable --->
<cfreturn getRepInstance(application._taffy.settings.serializer) />
<cfelseif application._taffy.factory.containsBean(arguments.repClass)>
<cfreturn application._taffy.factory.getBean(arguments.repClass) />
<cfelse>
<cfreturn createObject("component", arguments.repClass) />
</cfif>
</cffunction>
<cffunction name="addDebugData" access="package" output="false">
<cfargument name="data" type="any" />
<cfset request.debugData = arguments.data />
</cffunction>
</cfcomponent>
+35
View File
@@ -0,0 +1,35 @@
<!---
This script is used to proxy in static assets like JavaScript and CSS from the Taffy install folder without having to have them in the web root
or to include their full contents in the html body, as Taffy 1.x did.
--->
<cfparam name="url.a" default="" />
<cfswitch expression="#url.a#">
<cfcase value="jquery.min.js">
<cfcontent type="text/javascript" file="#expandPath('./jquery.min.js')#" reset="true" /><cfabort />
</cfcase>
<cfcase value="bootstrap.min.js">
<cfcontent type="text/javascript" file="#expandPath('./bootstrap.min.js')#" reset="true" /><cfabort />
</cfcase>
<cfcase value="dash.js">
<cfcontent type="text/javascript" file="#expandPath('./dash.js')#" reset="true" /><cfabort />
</cfcase>
<cfcase value="dash.css">
<cfcontent type="text/css" file="#expandPath('./dash.css')#" reset="true" /><cfabort />
</cfcase>
<cfcase value="loading.gif">
<cfcontent type="image/gif" file="#expandPath('./loading.gif')#" reset="true" /><cfabort />
</cfcase>
<cfdefaultcase>
<cfheader statuscode="404" statustext="Not Found" />
<cfcontent reset="true" /><cfabort />
</cfdefaultcase>
</cfswitch>
+6
View File
@@ -0,0 +1,6 @@
/**
* bootstrap.js v3.0.0 by @fat and @mdo
* Copyright 2013 Twitter Inc.
* http://www.apache.org/licenses/LICENSE-2.0
*/
if(!jQuery)throw new Error("Bootstrap requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]}}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one(a.support.transition.end,function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b()})}(window.jQuery),+function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function c(){f.trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one(a.support.transition.end,c).emulateTransitionEnd(150):c())};var d=a.fn.alert;a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("bs.alert");e||d.data("bs.alert",e=new c(this)),"string"==typeof b&&e[b].call(d)})},a.fn.alert.Constructor=c,a.fn.alert.noConflict=function(){return a.fn.alert=d,this},a(document).on("click.bs.alert.data-api",b,c.prototype.close)}(window.jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d)};b.DEFAULTS={loadingText:"loading..."},b.prototype.setState=function(a){var b="disabled",c=this.$element,d=c.is("input")?"val":"html",e=c.data();a+="Text",e.resetText||c.data("resetText",c[d]()),c[d](e[a]||this.options[a]),setTimeout(function(){"loadingText"==a?c.addClass(b).attr(b,b):c.removeClass(b).removeAttr(b)},0)},b.prototype.toggle=function(){var a=this.$element.closest('[data-toggle="buttons"]');if(a.length){var b=this.$element.find("input").prop("checked",!this.$element.hasClass("active")).trigger("change");"radio"===b.prop("type")&&a.find(".active").removeClass("active")}this.$element.toggleClass("active")};var c=a.fn.button;a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof c&&c;e||d.data("bs.button",e=new b(this,f)),"toggle"==c?e.toggle():c&&e.setState(c)})},a.fn.button.Constructor=b,a.fn.button.noConflict=function(){return a.fn.button=c,this},a(document).on("click.bs.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle"),b.preventDefault()})}(window.jQuery),+function(a){"use strict";var b=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},b.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},b.prototype.getActiveIndex=function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},b.prototype.to=function(b){var c=this,d=this.getActiveIndex();return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},b.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition.end&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},b.prototype.next=function(){return this.sliding?void 0:this.slide("next")},b.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},b.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}this.sliding=!0,f&&this.pause();var j=a.Event("slide.bs.carousel",{relatedTarget:e[0],direction:g});if(!e.hasClass("active")){if(this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid",function(){var b=a(i.$indicators.children()[i.getActiveIndex()]);b&&b.addClass("active")})),a.support.transition&&this.$element.hasClass("slide")){if(this.$element.trigger(j),j.isDefaultPrevented())return;e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid")},0)}).emulateTransitionEnd(600)}else{if(this.$element.trigger(j),j.isDefaultPrevented())return;d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return f&&this.cycle(),this}};var c=a.fn.carousel;a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c),g="string"==typeof c?c:f.slide;e||d.data("bs.carousel",e=new b(this,f)),"number"==typeof c?e.to(c):g?e[g]():f.interval&&e.pause().cycle()})},a.fn.carousel.Constructor=b,a.fn.carousel.noConflict=function(){return a.fn.carousel=c,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(b){var c,d=a(this),e=a(d.attr("data-target")||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"")),f=a.extend({},e.data(),d.data()),g=d.attr("data-slide-to");g&&(f.interval=!1),e.carousel(f),(g=d.attr("data-slide-to"))&&e.data("bs.carousel").to(g),b.preventDefault()}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var b=a(this);b.carousel(b.data())})})}(window.jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.DEFAULTS={toggle:!0},b.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},b.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b=a.Event("show.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.$parent&&this.$parent.find("> .panel > .in");if(c&&c.length){var d=c.data("bs.collapse");if(d&&d.transitioning)return;c.collapse("hide"),d||c.data("bs.collapse",null)}var e=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[e](0),this.transitioning=1;var f=function(){this.$element.removeClass("collapsing").addClass("in")[e]("auto"),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return f.call(this);var g=a.camelCase(["scroll",e].join("-"));this.$element.one(a.support.transition.end,a.proxy(f,this)).emulateTransitionEnd(350)[e](this.$element[0][g])}}},b.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?(this.$element[c](0).one(a.support.transition.end,a.proxy(d,this)).emulateTransitionEnd(350),void 0):d.call(this)}}},b.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var c=a.fn.collapse;a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c);e||d.data("bs.collapse",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.collapse.Constructor=b,a.fn.collapse.noConflict=function(){return a.fn.collapse=c,this},a(document).on("click.bs.collapse.data-api","[data-toggle=collapse]",function(b){var c,d=a(this),e=d.attr("data-target")||b.preventDefault()||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,""),f=a(e),g=f.data("bs.collapse"),h=g?"toggle":d.data(),i=d.attr("data-parent"),j=i&&a(i);g&&g.transitioning||(j&&j.find('[data-toggle=collapse][data-parent="'+i+'"]').not(d).addClass("collapsed"),d[f.hasClass("in")?"addClass":"removeClass"]("collapsed")),f.collapse(h)})}(window.jQuery),+function(a){"use strict";function b(){a(d).remove(),a(e).each(function(b){var d=c(a(this));d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown")),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown"))})}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}var d=".dropdown-backdrop",e="[data-toggle=dropdown]",f=function(b){a(b).on("click.bs.dropdown",this.toggle)};f.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){if("ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('<div class="dropdown-backdrop"/>').insertAfter(a(this)).on("click",b),f.trigger(d=a.Event("show.bs.dropdown")),d.isDefaultPrevented())return;f.toggleClass("open").trigger("shown.bs.dropdown"),e.focus()}return!1}},f.prototype.keydown=function(b){if(/(38|40|27)/.test(b.keyCode)){var d=a(this);if(b.preventDefault(),b.stopPropagation(),!d.is(".disabled, :disabled")){var f=c(d),g=f.hasClass("open");if(!g||g&&27==b.keyCode)return 27==b.which&&f.find(e).focus(),d.click();var h=a("[role=menu] li:not(.divider):visible a",f);if(h.length){var i=h.index(h.filter(":focus"));38==b.keyCode&&i>0&&i--,40==b.keyCode&&i<h.length-1&&i++,~i||(i=0),h.eq(i).focus()}}}};var g=a.fn.dropdown;a.fn.dropdown=function(b){return this.each(function(){var c=a(this),d=c.data("dropdown");d||c.data("dropdown",d=new f(this)),"string"==typeof b&&d[b].call(c)})},a.fn.dropdown.Constructor=f,a.fn.dropdown.noConflict=function(){return a.fn.dropdown=g,this},a(document).on("click.bs.dropdown.data-api",b).on("click.bs.dropdown.data-api",".dropdown form",function(a){a.stopPropagation()}).on("click.bs.dropdown.data-api",e,f.prototype.toggle).on("keydown.bs.dropdown.data-api",e+", [role=menu]",f.prototype.keydown)}(window.jQuery),+function(a){"use strict";var b=function(b,c){this.options=c,this.$element=a(b),this.$backdrop=this.isShown=null,this.options.remote&&this.$element.load(this.options.remote)};b.DEFAULTS={backdrop:!0,keyboard:!0,show:!0},b.prototype.toggle=function(a){return this[this.isShown?"hide":"show"](a)},b.prototype.show=function(b){var c=this,d=a.Event("show.bs.modal",{relatedTarget:b});this.$element.trigger(d),this.isShown||d.isDefaultPrevented()||(this.isShown=!0,this.escape(),this.$element.on("click.dismiss.modal",'[data-dismiss="modal"]',a.proxy(this.hide,this)),this.backdrop(function(){var d=a.support.transition&&c.$element.hasClass("fade");c.$element.parent().length||c.$element.appendTo(document.body),c.$element.show(),d&&c.$element[0].offsetWidth,c.$element.addClass("in").attr("aria-hidden",!1),c.enforceFocus();var e=a.Event("shown.bs.modal",{relatedTarget:b});d?c.$element.find(".modal-dialog").one(a.support.transition.end,function(){c.$element.focus().trigger(e)}).emulateTransitionEnd(300):c.$element.focus().trigger(e)}))},b.prototype.hide=function(b){b&&b.preventDefault(),b=a.Event("hide.bs.modal"),this.$element.trigger(b),this.isShown&&!b.isDefaultPrevented()&&(this.isShown=!1,this.escape(),a(document).off("focusin.bs.modal"),this.$element.removeClass("in").attr("aria-hidden",!0).off("click.dismiss.modal"),a.support.transition&&this.$element.hasClass("fade")?this.$element.one(a.support.transition.end,a.proxy(this.hideModal,this)).emulateTransitionEnd(300):this.hideModal())},b.prototype.enforceFocus=function(){a(document).off("focusin.bs.modal").on("focusin.bs.modal",a.proxy(function(a){this.$element[0]===a.target||this.$element.has(a.target).length||this.$element.focus()},this))},b.prototype.escape=function(){this.isShown&&this.options.keyboard?this.$element.on("keyup.dismiss.bs.modal",a.proxy(function(a){27==a.which&&this.hide()},this)):this.isShown||this.$element.off("keyup.dismiss.bs.modal")},b.prototype.hideModal=function(){var a=this;this.$element.hide(),this.backdrop(function(){a.removeBackdrop(),a.$element.trigger("hidden.bs.modal")})},b.prototype.removeBackdrop=function(){this.$backdrop&&this.$backdrop.remove(),this.$backdrop=null},b.prototype.backdrop=function(b){var c=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var d=a.support.transition&&c;if(this.$backdrop=a('<div class="modal-backdrop '+c+'" />').appendTo(document.body),this.$element.on("click.dismiss.modal",a.proxy(function(a){a.target===a.currentTarget&&("static"==this.options.backdrop?this.$element[0].focus.call(this.$element[0]):this.hide.call(this))},this)),d&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),!b)return;d?this.$backdrop.one(a.support.transition.end,b).emulateTransitionEnd(150):b()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),a.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(a.support.transition.end,b).emulateTransitionEnd(150):b()):b&&b()};var c=a.fn.modal;a.fn.modal=function(c,d){return this.each(function(){var e=a(this),f=e.data("bs.modal"),g=a.extend({},b.DEFAULTS,e.data(),"object"==typeof c&&c);f||e.data("bs.modal",f=new b(this,g)),"string"==typeof c?f[c](d):g.show&&f.show(d)})},a.fn.modal.Constructor=b,a.fn.modal.noConflict=function(){return a.fn.modal=c,this},a(document).on("click.bs.modal.data-api",'[data-toggle="modal"]',function(b){var c=a(this),d=c.attr("href"),e=a(c.attr("data-target")||d&&d.replace(/.*(?=#[^\s]+$)/,"")),f=e.data("modal")?"toggle":a.extend({remote:!/#/.test(d)&&d},e.data(),c.data());b.preventDefault(),e.modal(f,this).one("hide",function(){c.is(":visible")&&c.focus()})}),a(document).on("show.bs.modal",".modal",function(){a(document.body).addClass("modal-open")}).on("hidden.bs.modal",".modal",function(){a(document.body).removeClass("modal-open")})}(window.jQuery),+function(a){"use strict";var b=function(a,b){this.type=this.options=this.enabled=this.timeout=this.hoverState=this.$element=null,this.init("tooltip",a,b)};b.DEFAULTS={animation:!0,placement:"top",selector:!1,template:'<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',trigger:"hover focus",title:"",delay:0,html:!1,container:!1},b.prototype.init=function(b,c,d){this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d);for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focus",i="hover"==g?"mouseleave":"blur";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},b.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},b.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type);return clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show),void 0):c.show()},b.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type);return clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide),void 0):c.hide()},b.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){if(this.$element.trigger(b),b.isDefaultPrevented())return;var c=this.tip();this.setContent(),this.options.animation&&c.addClass("fade");var d="function"==typeof this.options.placement?this.options.placement.call(this,c[0],this.$element[0]):this.options.placement,e=/\s?auto?\s?/i,f=e.test(d);f&&(d=d.replace(e,"")||"top"),c.detach().css({top:0,left:0,display:"block"}).addClass(d),this.options.container?c.appendTo(this.options.container):c.insertAfter(this.$element);var g=this.getPosition(),h=c[0].offsetWidth,i=c[0].offsetHeight;if(f){var j=this.$element.parent(),k=d,l=document.documentElement.scrollTop||document.body.scrollTop,m="body"==this.options.container?window.innerWidth:j.outerWidth(),n="body"==this.options.container?window.innerHeight:j.outerHeight(),o="body"==this.options.container?0:j.offset().left;d="bottom"==d&&g.top+g.height+i-l>n?"top":"top"==d&&g.top-l-i<0?"bottom":"right"==d&&g.right+h>m?"left":"left"==d&&g.left-h<o?"right":d,c.removeClass(k).addClass(d)}var p=this.getCalculatedOffset(d,g,h,i);this.applyPlacement(p,d),this.$element.trigger("shown.bs."+this.type)}},b.prototype.applyPlacement=function(a,b){var c,d=this.tip(),e=d[0].offsetWidth,f=d[0].offsetHeight,g=parseInt(d.css("margin-top"),10),h=parseInt(d.css("margin-left"),10);isNaN(g)&&(g=0),isNaN(h)&&(h=0),a.top=a.top+g,a.left=a.left+h,d.offset(a).addClass("in");var i=d[0].offsetWidth,j=d[0].offsetHeight;if("top"==b&&j!=f&&(c=!0,a.top=a.top+f-j),/bottom|top/.test(b)){var k=0;a.left<0&&(k=-2*a.left,a.left=0,d.offset(a),i=d[0].offsetWidth,j=d[0].offsetHeight),this.replaceArrow(k-e+i,i,"left")}else this.replaceArrow(j-f,j,"top");c&&d.offset(a)},b.prototype.replaceArrow=function(a,b,c){this.arrow().css(c,a?50*(1-a/b)+"%":"")},b.prototype.setContent=function(){var a=this.tip(),b=this.getTitle();a.find(".tooltip-inner")[this.options.html?"html":"text"](b),a.removeClass("fade in top bottom left right")},b.prototype.hide=function(){function b(){"in"!=c.hoverState&&d.detach()}var c=this,d=this.tip(),e=a.Event("hide.bs."+this.type);return this.$element.trigger(e),e.isDefaultPrevented()?void 0:(d.removeClass("in"),a.support.transition&&this.$tip.hasClass("fade")?d.one(a.support.transition.end,b).emulateTransitionEnd(150):b(),this.$element.trigger("hidden.bs."+this.type),this)},b.prototype.fixTitle=function(){var a=this.$element;(a.attr("title")||"string"!=typeof a.attr("data-original-title"))&&a.attr("data-original-title",a.attr("title")||"").attr("title","")},b.prototype.hasContent=function(){return this.getTitle()},b.prototype.getPosition=function(){var b=this.$element[0];return a.extend({},"function"==typeof b.getBoundingClientRect?b.getBoundingClientRect():{width:b.offsetWidth,height:b.offsetHeight},this.$element.offset())},b.prototype.getCalculatedOffset=function(a,b,c,d){return"bottom"==a?{top:b.top+b.height,left:b.left+b.width/2-c/2}:"top"==a?{top:b.top-d,left:b.left+b.width/2-c/2}:"left"==a?{top:b.top+b.height/2-d/2,left:b.left-c}:{top:b.top+b.height/2-d/2,left:b.left+b.width}},b.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},b.prototype.tip=function(){return this.$tip=this.$tip||a(this.options.template)},b.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},b.prototype.validate=function(){this.$element[0].parentNode||(this.hide(),this.$element=null,this.options=null)},b.prototype.enable=function(){this.enabled=!0},b.prototype.disable=function(){this.enabled=!1},b.prototype.toggleEnabled=function(){this.enabled=!this.enabled},b.prototype.toggle=function(b){var c=b?a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type):this;c.tip().hasClass("in")?c.leave(c):c.enter(c)},b.prototype.destroy=function(){this.hide().$element.off("."+this.type).removeData("bs."+this.type)};var c=a.fn.tooltip;a.fn.tooltip=function(c){return this.each(function(){var d=a(this),e=d.data("bs.tooltip"),f="object"==typeof c&&c;e||d.data("bs.tooltip",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.tooltip.Constructor=b,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=c,this}}(window.jQuery),+function(a){"use strict";var b=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");b.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:'<div class="popover"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>'}),b.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),b.prototype.constructor=b,b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content")[this.options.html?"html":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},b.prototype.hasContent=function(){return this.getTitle()||this.getContent()},b.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},b.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")},b.prototype.tip=function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip};var c=a.fn.popover;a.fn.popover=function(c){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof c&&c;e||d.data("bs.popover",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.popover.Constructor=b,a.fn.popover.noConflict=function(){return a.fn.popover=c,this}}(window.jQuery),+function(a){"use strict";function b(c,d){var e,f=a.proxy(this.process,this);this.$element=a(c).is("body")?a(window):a(c),this.$body=a("body"),this.$scrollElement=this.$element.on("scroll.bs.scroll-spy.data-api",f),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||(e=a(c).attr("href"))&&e.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.offsets=a([]),this.targets=a([]),this.activeTarget=null,this.refresh(),this.process()}b.DEFAULTS={offset:10},b.prototype.refresh=function(){var b=this.$element[0]==window?"offset":"position";this.offsets=a([]),this.targets=a([]);var c=this;this.$body.find(this.selector).map(function(){var d=a(this),e=d.data("target")||d.attr("href"),f=/^#\w/.test(e)&&a(e);return f&&f.length&&[[f[b]().top+(!a.isWindow(c.$scrollElement.get(0))&&c.$scrollElement.scrollTop()),e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){c.offsets.push(this[0]),c.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,d=c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(b>=d)return g!=(a=f.last()[0])&&this.activate(a);for(a=e.length;a--;)g!=f[a]&&b>=e[a]&&(!e[a+1]||b<=e[a+1])&&this.activate(f[a])},b.prototype.activate=function(b){this.activeTarget=b,a(this.selector).parents(".active").removeClass("active");var c=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',d=a(c).parents("li").addClass("active");d.parent(".dropdown-menu").length&&(d=d.closest("li.dropdown").addClass("active")),d.trigger("activate")};var c=a.fn.scrollspy;a.fn.scrollspy=function(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.scrollspy.Constructor=b,a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=c,this},a(window).on("load",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);b.scrollspy(b.data())})})}(window.jQuery),+function(a){"use strict";var b=function(b){this.element=a(b)};b.prototype.show=function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.attr("data-target");if(d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),!b.parent("li").hasClass("active")){var e=c.find(".active:last a")[0],f=a.Event("show.bs.tab",{relatedTarget:e});if(b.trigger(f),!f.isDefaultPrevented()){var g=a(d);this.activate(b.parent("li"),c),this.activate(g,g.parent(),function(){b.trigger({type:"shown.bs.tab",relatedTarget:e})})}}},b.prototype.activate=function(b,c,d){function e(){f.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),g?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var f=c.find("> .active"),g=d&&a.support.transition&&f.hasClass("fade");g?f.one(a.support.transition.end,e).emulateTransitionEnd(150):e(),f.removeClass("in")};var c=a.fn.tab;a.fn.tab=function(c){return this.each(function(){var d=a(this),e=d.data("bs.tab");e||d.data("bs.tab",e=new b(this)),"string"==typeof c&&e[c]()})},a.fn.tab.Constructor=b,a.fn.tab.noConflict=function(){return a.fn.tab=c,this},a(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(b){b.preventDefault(),a(this).tab("show")})}(window.jQuery),+function(a){"use strict";var b=function(c,d){this.options=a.extend({},b.DEFAULTS,d),this.$window=a(window).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(c),this.affixed=this.unpin=null,this.checkPosition()};b.RESET="affix affix-top affix-bottom",b.DEFAULTS={offset:0},b.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},b.prototype.checkPosition=function(){if(this.$element.is(":visible")){var c=a(document).height(),d=this.$window.scrollTop(),e=this.$element.offset(),f=this.options.offset,g=f.top,h=f.bottom;"object"!=typeof f&&(h=g=f),"function"==typeof g&&(g=f.top()),"function"==typeof h&&(h=f.bottom());var i=null!=this.unpin&&d+this.unpin<=e.top?!1:null!=h&&e.top+this.$element.height()>=c-h?"bottom":null!=g&&g>=d?"top":!1;this.affixed!==i&&(this.unpin&&this.$element.css("top",""),this.affixed=i,this.unpin="bottom"==i?e.top-d:null,this.$element.removeClass(b.RESET).addClass("affix"+(i?"-"+i:"")),"bottom"==i&&this.$element.offset({top:document.body.offsetHeight-h-this.$element.height()}))}};var c=a.fn.affix;a.fn.affix=function(c){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof c&&c;e||d.data("bs.affix",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.affix.Constructor=b,a.fn.affix.noConflict=function(){return a.fn.affix=c,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var b=a(this),c=b.data();c.offset=c.offset||{},c.offsetBottom&&(c.offset.bottom=c.offsetBottom),c.offsetTop&&(c.offset.top=c.offsetTop),b.affix(c)})})}(window.jQuery);
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+483
View File
@@ -0,0 +1,483 @@
if (!String.prototype.trim) {
String.prototype.trim = function () {
return this.replace(/^[\s\xA0]+|[\s\xA0]+$/g, '');
};
}
$(function(){
//hide request body form field for GET/DELETE on accordion open
$("#resources").on('shown.bs.collapse', function(e){
var resource = $("#resources .in .resource");
var method = resource.find('.reqMethod option:checked').html();
if (method === 'GET' || method === 'DELETE'){
resource.find('.reqBody').hide('fast');
resource.find('.queryParams').addClass('active');
}else{
var args = window.taffy.resources[resource.data('beanName')][method.toLowerCase()];
var ta = resource.find('.reqBody').show('fast').find('textarea');
ta.val(JSON.stringify(args, null, 3));
resource.find('.queryParams').removeClass('active');
}
});
//hide request body form field for GET/DELETE on method change
$(".resource .reqMethod").on('change', function(){
var resource = $(this).closest('.resource');
var method = resource.find('.reqMethod option:checked').html();
if (method === 'GET' || method === 'DELETE' || method == 'OPTIONS'){
resource.find('.reqBody').hide('fast');
}else{
var args = window.taffy.resources[resource.data('beanName')][method.toLowerCase()];
var ta = resource.find('.reqBody').show('fast').find('textarea');
ta.val(JSON.stringify(args, null, 3));
resource.find('.queryParams').find('input').val('');
}
});
$(".addParam").click(function(){
var resource = $(this).closest('.resource')
,params = resource.find('.queryParams');
var tmpl = '<div class="qparam row form-group"><div class="col-md-4"><input class="form-control input-small paramName" /></div><div class="col-md-1 micro">=</div><div class="col-md-4"><input class="form-control input-small paramValue" /></div><div class="col-md-2"><button class="btn delParam" tabindex="-1">-</button></div></div>';
params.append(tmpl);
});
$(".resource").on('click', '.delParam', function(){
var row = $(this).closest('.row');
row.remove();
});
//interpolate resource uri token values as they're typed
$(".resource").on('keyup', 'input', function(e){
var $this = $(this)
,resource = $this.closest('.resource')
,tokens = params( resource.find('.reqTokens form').serialize() )
,q = qParams(resource)
,uri = resource.data('uri')
for (var t in tokens){
if (tokens[t] === '')
delete tokens[t];
}
var result = uri.supplant(tokens);
result += (q.length) ? '?' + q : '';
resource.find('.resourceUri').val(result);
});
$(".submitRequest").click(function(){
var submit = $(this)
,resource = submit.closest('.resource')
,reset = resource.find('.resetRequest')
,loading = resource.find('.progress')
,response = resource.find('.response')
,basicAuth = resource.find(".basicAuth");
//validate tokens
resource.find('.has-error').removeClass('has-error');
var tokenErrors = resource.find('.tokenErrors');
var tokens = resource.find('.reqTokens input');
tokenErrors.empty();
for (var t=0;t<tokens.length;t++){
var tok = $(tokens[t]);
if (tok.val().length === 0){
tok.closest('.form-group').addClass('has-error').focus();
tokenErrors.append('<div class="alert alert-danger">' + tok.attr('name') + ' is required</div>');
}
}
if (resource.find('.reqTokens .has-error').length > 0){
return false;
}
loading.show();
submit.attr('disabled','disabled');
response.hide();
//interpolate the full request path
var uri = resource.data('uri')
,form = params( resource.find('.reqTokens form').serialize() )
,path = uri.supplant(form);
var verb = resource.find('.reqMethod option:checked').val();
var body = (verb === 'GET' || verb === 'DELETE') ? qParams(resource) : resource.find('.reqBody textarea').val();
var reqHeaders = resource.find('.requestHeaders').val().replace(/\r/g, '').split('\n');
var headers = {
Accept: resource.find('.reqFormat option:checked').val()
,"Content-Type": (verb === 'GET' || verb === 'DELETE') ? "application/x-www-form-urlencoded" : "application/json"
};
for (var h in reqHeaders){
var kv = reqHeaders[h].trim().split(':');
if (kv[0].trim().length == 0){
continue;
}
if (kv.length == 2){
headers[ kv[0].trim() ] = kv[1].trim();
}else if (kv.length == 1){
headers [kv[0].trim() ] = "";
}else{
var k = kv.shift().trim();
var v = kv.join(':').trim();
headers[ k ] = v;
}
}
var basicAuthUsername = basicAuth.find("input[name=username]").val();
var basicAuthPassword = basicAuth.find("input[name=password]").val();
if(basicAuthUsername.length && basicAuthPassword.length){
headers["Authorization"] = "Basic " + Base64.encode(basicAuthUsername + ":" + basicAuthPassword);
}
submitRequest(verb, path, headers, body, function(timeSpent, status, headers, body){
loading.hide();
submit.removeAttr('disabled');
reset.show();
headers = parseHeaders(headers);
if (headers['content-type'].indexOf('application/json') > -1 || headers['content-type'].indexOf('text/json') > -1 || headers['content-type'].indexOf('application/vnd.api+json') > -1){
//indentation!
if (body.length){
body = JSON.stringify(JSON.parse(body), null, 3);
// only do syntax highlighting if hljs is defined
if (typeof hljs === 'undefined') {
body = body.split('\n')
.join('<br/>')
.replace(/\s/g,'&nbsp;');
} else {
// syntax highlight json and then replace spaces at the start of each line (or after <br/>) with &nbsp;
body = hljs.highlight("json", body).value;
body = body.split('\n')
.join('<br/>')
.replace(/(\<br\/\>)(\s+)/g, function(match, p1, p2, offset, string){
return [p1, p2.replace(/\s/g,'&nbsp;')].join('');
});
}
}
}
var headerRow = response.find('.responseHeaders');
headerRow.empty();
response.show();
var sortable = [];
for (var h in headers){
sortable.push(h);
}
sortable.sort();
for (var h in sortable){
headerRow.append('<div class="row"><div class="col-md-5 headerName">' + sortable[h] + ':</div><div class="col-md-7 headerVal">' + headers[sortable[h]] + '</div></div>');
}
response.find('.responseTime').html('Request took ' + timeSpent + 'ms');
response.find('.responseStatus').html(status);
response.find('.responseBody').html(body);
});
});
$(".resetRequest").click(function(){
var reset = $(this)
,resource = reset.closest('.resource')
,response = resource.find('.response')
,tokens = resource.find('.reqTokens form input')
,params = resource.find('.queryParams input')
,uri = resource.data('uri');
response.hide();
reset.hide();
resource.find('.resourceUri').val(uri);
tokens.each(function(){
$(this).val('');
});
params.each(function(){
$(this).val('');
})
});
});
function qParams(resource){
var validParams = [];
resource.find('.qparam').each(function(){
var $this = $(this), n = $this.find('.paramName'), v = $this.find('.paramValue');
var nameLen = n.val().length, valLen = v.val().length;
if (nameLen && valLen){
validParams.push(encodeURIComponent(n.val()) + '=' + encodeURIComponent(v.val()));
$this.removeClass('has-error');
}else{
$this.addClass('has-error');
}
});
return validParams.join('&');
}
function toggleStackTrace(id){
console.log('toggling %s', id);
$('#' + id).toggle();
}
function params(query){
var parameters = {}, parameter;
if (query.length > 1){
query = query.split('&');
for (var i = 0; i < query.length; i++) {
parameter = query[i].split("=");
if (parameter.length === 1) { parameter[1] = ""; }
parameters[decodeURIComponent(parameter[0])] = decodeURIComponent(parameter[1]);
}
}
return parameters;
}
function parseHeaders(h){
var out = {};
var chunks = h.toLowerCase().split('\n');
for (var i=0,j=chunks.length; i<j; i++){
var bits = chunks[i].split(': ');
if (bits[0].length)
out[bits[0].toLowerCase()] = bits[1];
}
return out;
}
String.prototype.supplant = function (o) {
return this.replace(/{(.*?)(}(?=\/)|}$)/g,
function (a, b) {
// We need to split on the : if we're using custom token regular expressions.
var r = o[ b.split(':')[ 0 ] ];
return typeof r === 'string' || typeof r === 'number' ? r : a;
}
);
};
/**
*
* Base64 encode / decode
* http://www.webtoolkit.info/
*
**/
var Base64 = {
// private property
_keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
// public method for encoding
encode : function (input) {
var output = "";
var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
var i = 0;
input = Base64._utf8_encode(input);
while (i < input.length) {
chr1 = input.charCodeAt(i++);
chr2 = input.charCodeAt(i++);
chr3 = input.charCodeAt(i++);
enc1 = chr1 >> 2;
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
enc4 = chr3 & 63;
if (isNaN(chr2)) {
enc3 = enc4 = 64;
} else if (isNaN(chr3)) {
enc4 = 64;
}
output = output +
this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) +
this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4);
}
return output;
},
// public method for decoding
decode : function (input) {
var output = "";
var chr1, chr2, chr3;
var enc1, enc2, enc3, enc4;
var i = 0;
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
while (i < input.length) {
enc1 = this._keyStr.indexOf(input.charAt(i++));
enc2 = this._keyStr.indexOf(input.charAt(i++));
enc3 = this._keyStr.indexOf(input.charAt(i++));
enc4 = this._keyStr.indexOf(input.charAt(i++));
chr1 = (enc1 << 2) | (enc2 >> 4);
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
chr3 = ((enc3 & 3) << 6) | enc4;
output = output + String.fromCharCode(chr1);
if (enc3 != 64) {
output = output + String.fromCharCode(chr2);
}
if (enc4 != 64) {
output = output + String.fromCharCode(chr3);
}
}
output = Base64._utf8_decode(output);
return output;
},
// private method for UTF-8 encoding
_utf8_encode : function (string) {
string = string.replace(/\r\n/g,"\n");
var utftext = "";
for (var n = 0; n < string.length; n++) {
var c = string.charCodeAt(n);
if (c < 128) {
utftext += String.fromCharCode(c);
}
else if((c > 127) && (c < 2048)) {
utftext += String.fromCharCode((c >> 6) | 192);
utftext += String.fromCharCode((c & 63) | 128);
}
else {
utftext += String.fromCharCode((c >> 12) | 224);
utftext += String.fromCharCode(((c >> 6) & 63) | 128);
utftext += String.fromCharCode((c & 63) | 128);
}
}
return utftext;
},
// private method for UTF-8 decoding
_utf8_decode : function (utftext) {
var string = "";
var i = 0;
var c = c1 = c2 = 0;
while ( i < utftext.length ) {
c = utftext.charCodeAt(i);
if (c < 128) {
string += String.fromCharCode(c);
i++;
}
else if((c > 191) && (c < 224)) {
c2 = utftext.charCodeAt(i+1);
string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
i += 2;
}
else {
c2 = utftext.charCodeAt(i+1);
c3 = utftext.charCodeAt(i+2);
string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
i += 3;
}
}
return string;
}
}
function expandingFormElements(){
var updateTabindexes = function(){
var invisible_parent_class = '.expandable';
var parent_visible_modifier_class = '.active';
var nextTabIndex = 1;
$('input,select,textarea,button').not('[type=hidden]').each(function(elem){
var $elem = $( this );
if ( $elem.parents( invisible_parent_class ).length > 0 ){
//could be invisible...
if ( $elem.parents( invisible_parent_class + parent_visible_modifier_class ).length > 0 ){
//is visible, give it a tab index
$elem.attr( 'tabindex', nextTabIndex );
nextTabIndex++;
}
}else{
$elem.attr( 'tabindex', nextTabIndex );
nextTabIndex++;
}
});
}
var handleExpansion = function(e){
var $this = $(this)
,$target = $( $this.data('target') )
,isVisible = $target.hasClass('active');
if ( $this.is('a') ){
e.preventDefault();
if (isVisible){
$target.removeClass('active');
$this.closest('.hide-on-expand').show('fast');
}else{
$target.addClass('active');
$this.closest('.hide-on-expand').hide('fast');
}
updateTabindexes();
return false;
}else if ( $this.is('input[type=checkbox]') ){
if ( $this.prop('checked') ){
$target.addClass('active');
}else{
$target.removeClass('active');
}
}else if ( $this.is('input[type=radio]') ){
var expand = $this.data('expand') && $this.is(':checked');
if ( expand ){
$target.addClass('active');
}else{
$target.removeClass('active');
}
}else if ( $this.is('select') ){
var selectedOption = $this.find('option:selected');
var expand = selectedOption.data('expand');
if ( expand ){
$target.addClass('active');
}else{
$target.removeClass('active');
}
}else if ( $this.is('input[type=text]') ){
if ( $this.val().trim().length > 0 ){
$target.addClass('active');
}else{
$target.removeClass('active');
}
}
updateTabindexes();
}
$('a.expander').on('click', handleExpansion);
$('input.expander,select.expander').on('keyup change', handleExpansion);
//initialize starting state of text fields
$('input.expander,select.expander').each(function(){
handleExpansion.apply(this);
});
}
expandingFormElements();
+228
View File
@@ -0,0 +1,228 @@
@import "bootstrap.min.less";
body {
padding-bottom: 30px;
font-size: 16px;
}
.ver {
font-size: 0.9em;
font-weight: bold;
display: inline-block;
position: relative;
top: -16px;
left: 187px;
}
.masthead {
button {
float: right;
margin-left: 10px;
}
}
.stackTrace {
display: none;
}
.alert {
code {
white-space: normal;
}
}
.modal {
.label {
float: right;
}
.col-sm-6 {
margin-top: 10px;
}
dl {
margin: 0;
}
ul {
padding: 0 0 0 15px;
}
}
.runner .well.resource {
margin: 15px 0 0 0;
}
#resources {
margin-top: -20px;
.accordion-toggle {
display: block;
margin: -10px -15px;
padding: 10px 15px;
&:hover {
text-decoration: none;
background: #ddd;
}
}
.resource {
h4 {
span {
font-size: 0.6em;
}
}
}
.verb {
float: right;
margin-left: 5px;
padding-top: 4px;
margin-top: -18px;
&.label-default {
background-color: #ccc;
}
}
.showDocs {
float: right;
}
.hideDocs {
float: right;
}
.col-md-6 {
.showDocs {
display: none;
}
}
.submitRequest {
float: right;
margin-left: 10px;
}
.resetRequest {
display: none;
float: right;
margin-left: 10px;
}
.micro {
padding: 5px 0;
width: 10px;
text-align: center;
}
.reqBody {
display: none;
}
.doc {
display: block;
color: #000;
}
.doc.hint {
padding-left: 20px;
color: #717171;
}
.progress {
display: none;
margin-top: 10px;
}
.response {
display: none;
}
.responseTime {
float: right;
font-size: 0.8em;
margin-top: 5px;
margin-bottom: -2px;
}
.responseBody {
white-space: nowrap;
}
pre {
overflow-x: scroll;
width: 100%;
}
.headerName {
font-weight: bold;
text-align: right;
}
.headerName,
.headerVal {
font-size: 0.8em;
line-height: 1.6em;
}
select {
width: 85px;
display: inline-block;
}
textarea {
font-family: monospace;
font-size: 0.9em;
}
}
.reqMethod {
float: left;
}
.resourceUri {
width: 100%;
max-width: 54%;
// margin: .5em 0;
margin-left: 90px;
margin-top: 0;
font-family: monospace;
}
.response {
display: none;
label {
display: block;
text-align: right;
}
}
a.expander {
cursor: pointer;
}
.expandable {
max-height: 0;
overflow: hidden;
display: block;
margin-bottom: 0;
-webkit-transition: max-height 700ms cubic-bezier(0.77, 0, 0.175, 1);
-moz-transition: max-height 700ms cubic-bezier(0.77, 0, 0.175, 1);
-o-transition: max-height 700ms cubic-bezier(0.77, 0, 0.175, 1);
transition: max-height 700ms cubic-bezier(0.77, 0, 0.175, 1); /* easeInOutQuart */
-webkit-transition-timing-function: cubic-bezier(0.77, 0, 0.175, 1);
-moz-transition-timing-function: cubic-bezier(0.77, 0, 0.175, 1);
-o-transition-timing-function: cubic-bezier(0.77, 0, 0.175, 1);
transition-timing-function: cubic-bezier(
0.77,
0,
0.175,
1
); /* easeInOutQuart */
&.active {
max-height: 1000px;
margin-bottom: 15px;
}
}
.tab-content {
padding-top: 10px;
}
.verbHeading {
font-size: 1.6em;
font-weight: bold;
text-transform: capitalize;
}
.inputs-wrapper {
padding-left: 20px;
padding-top: 10px;
}
.resourceUriPath {
float: right;
margin-top: -15px;
}
.queryParams .form-group:last-child {
margin-bottom: 0;
}
+673
View File
@@ -0,0 +1,673 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Taffy Dashboard</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="https://fav.farm/🍬" />
<style>
<cfinclude template="dash.css" />
<cfinclude template="highlight-github.min.css" />
</style>
</head>
<body>
<script>
window.taffy = { resources: {} };
</script>
<div class="container">
<div class="masthead">
<button id="docs" class="btn btn-success" onclick="window.location.href = '<cfoutput>#CGI.SCRIPT_NAME#</cfoutput>?docs'">Documentation</button>
<button id="reload" class="btn btn-info">Reload API Cache</button>
<button data-toggle="modal" data-target="#config" class="btn btn-default">Config</button>
<h1>API Dashboard</h1>
<span class="ver text-muted">Taffy <cfoutput>#application._taffy.version#</cfoutput></span>
</div>
<!--- config modal --->
<div class="modal fade" id="config" tabindex="-1" role="dialog" aria-labelledby="frameworkConfig" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title" id="frameworkConfig">Framework Configuration</h4>
</div>
<div class="modal-body" style="padding:0">
<cfoutput>
<div class="table-responsive">
<table class="table table-striped" style="margin-bottom:0">
<tr>
<td><strong>Reload on every request:</strong></td>
<td><a class="label label-default" target="_blank" rel="noreferrer noopener" href="<cfoutput>#getDocUrl('reloadOnEveryRequest')#</cfoutput>">?</a>
#yesNoFormat(application._taffy.settings.reloadOnEveryRequest)#</td>
</tr>
<tr>
<td><strong>Return Exceptions as JSON:</strong></td>
<td><a class="label label-default" target="_blank" rel="noreferrer noopener" href="<cfoutput>#getDocUrl('returnExceptionsAsJson')#</cfoutput>">?</a>
#yesNoFormat(application._taffy.settings.returnExceptionsAsJson)#</td>
</tr>
<tr>
<td><strong>CORS:</strong></td>
<td><a class="label label-default" target="_blank" rel="noreferrer noopener" href="<cfoutput>#getDocUrl('allowCrossDomain')#</cfoutput>">?</a>
<cfif application._taffy.settings.allowCrossDomain EQ 'false'>No<cfelse>Yes</cfif></td>
</tr>
<tr>
<td><strong>E-Tags:</strong></td>
<td><a class="label label-default" target="_blank" rel="noreferrer noopener" href="<cfoutput>#getDocUrl('useEtags')#</cfoutput>">?</a>
#yesNoFormat(application._taffy.settings.useEtags)#</td>
</tr>
<tr>
<td><strong>JSONP:</strong></td>
<td><a class="label label-default" target="_blank" rel="noreferrer noopener" href="<cfoutput>#getDocUrl('jsonp')#</cfoutput>">?</a>
<cfif application._taffy.settings.jsonp eq false>No<cfelse>?<strong>#application._taffy.settings.jsonp#=</strong>...
</cfif></td>
</tr>
<tr>
<td><strong>Endpoint URL Param:</strong></td>
<td><a class="label label-default" target="_blank" rel="noreferrer noopener" href="<cfoutput>#getDocUrl('endpointURLParam')#</cfoutput>">?</a>
#application._taffy.settings.endpointURLParam#</td>
</tr>
<tr>
<td><strong>Serializer:</strong></td>
<td><a class="label label-default" target="_blank" rel="noreferrer noopener" href="<cfoutput>#getDocUrl('serializer')#</cfoutput>">?</a>
#application._taffy.settings.serializer#</td>
</tr>
<tr>
<td><strong>Return Formats:</strong></td>
<td><ul>
<cfloop list="#structKeyList(application._taffy.settings.mimeTypes)#" index="local.m">
<li>#local.m#</li>
</cfloop>
</ul></td>
</tr>
<tr>
<td><strong>Global Headers:</strong></td>
<td><a class="label label-default" target="_blank" rel="noreferrer noopener" href="<cfoutput>#getDocUrl('globalHeaders')#</cfoutput>">?</a>
<dl>
<cfloop list="#structKeyList(application._taffy.settings.globalHeaders)#" index="local.h">
<dt>#local.h#</dt>
<dd>#application._taffy.settings.globalHeaders[local.h]#</dd>
</cfloop>
</dl>
<cfif structIsEmpty(application._taffy.settings.globalHeaders)>
None
</cfif></td>
</tr>
<tr>
<td><strong>Exception Log Adapter:</strong></td>
<td><a class="label label-default" target="_blank" rel="noreferrer noopener" href="<cfoutput>#getDocUrl('exceptionLogAdapter')#</cfoutput>">?</a>
#application._taffy.settings.exceptionLogAdapter#</td>
</tr>
<tr>
<td><strong>Exception Log Adapter Config:</strong></td>
<td><a class="label label-default" target="_blank" rel="noreferrer noopener" href="<cfoutput>#getDocUrl('exceptionLogAdapterConfig')#</cfoutput>">?</a>
<cfif isSimpleValue(application._taffy.settings.exceptionLogAdapterConfig)>
#application._taffy.settings.exceptionLogAdapterConfig#
<cfelse>
<dl>
<cfloop list="#structKeyList(application._taffy.settings.exceptionLogAdapterConfig)#" index="local.k">
<dt>#local.k#</dt>
<dd>#application._taffy.settings.exceptionLogAdapterConfig[local.k]#</dd>
</cfloop>
</dl>
</cfif></td>
</tr>
<tr>
<td><strong>Unhandled Paths:</strong></td>
<td><ul>
<cfloop list="#application._taffy.settings.unhandledPaths#" index="local.p">
<li>#local.p#</li>
</cfloop>
</ul></td>
</tr>
</table>
</div>
</cfoutput>
<div class="clearfix"></div>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!--- alerts --->
<div class="row" id="alerts">
<cfif structKeyExists(application._taffy, "status")
and structKeyExists(application._taffy.status, "skippedResources")
and arrayLen(application._taffy.status.skippedResources) gt 0>
<cfoutput>
<cfloop from="1" to="#arrayLen(application._taffy.status.skippedResources)#" index="local.i">
<cfset local.err = application._taffy.status.skippedResources[local.i] />
<cfset local.exceptionHasErrorCode = structKeyExists(local.err, "Exception") AND structKeyExists(local.err.Exception, "ErrorCode")>
<cfset local.errorCode = "" />
<cfif local.exceptionHasErrorCode>
<cfset local.errorCode = local.err.Exception.ErrorCode>
</cfif>
<div class="alert alert-warning">
<cfif local.errorCode EQ "taffy.resources.DuplicateUriPattern">
<strong class="label label-warning"><cfoutput>#local.err.resource#</cfoutput></strong> contains a conflicting URI.
<cfelseif local.errorcode EQ "taffy.resources.URIDoesntBeginWithForwardSlash">
<strong class="label label-warning"><cfoutput>#local.err.resource#</cfoutput></strong> should have a URI that begins with a forward slash.
<cfelse>
<strong class="label label-warning"><cfoutput>#local.err.resource#</cfoutput></strong> contains a syntax error.
<cfif structKeyExists(local.err.exception, 'tagContext')>
<strong>Error on line #local.err.exception.tagcontext[1].line#:</strong>
</cfif>
</cfif>
<hr/>
<code>
<cfif structKeyExists(local.err.exception, 'message')>#local.err.exception.message#</cfif>
<cfif structKeyExists(local.err.exception, 'detail')><br/><br/>#local.err.exception.detail#</cfif>
</code>
<hr/>
<cfset local.stack_id = createUUID() />
<a href="javascript:toggleStackTrace('#local.stack_id#');">Toggle Stack Trace</a>
<br/>
<div class="stackTrace" id="#local.stack_id#">
<cfdump var="#local.err.exception.tagcontext#" />
</div>
Reload the API Cache after resolving this error.
</div>
</cfloop>
</cfoutput>
</cfif>
</div><!-- /#alerts -->
<div class="row" id="resources">
<h3>
Resources:
<input type="text" id="resourceSearch" placeholder="Filter... (ESC to clear)" class="form-control" autocomplete="off" style="width:50%; display: inline-block;" />
</h3>
<div class="panel-group" id="resourcesAccordion">
<cfoutput>
<cfloop from="1" to="#arrayLen(application._taffy.uriMatchOrder)#" index="local.resource">
<cfset local.currentResource = application._taffy.endpoints[application._taffy.uriMatchOrder[local.resource]] />
<cfset local.resourceHTTPID = rereplace(local.currentResource.beanName & "_" & hash(local.currentResource.srcURI), "[^0-9a-zA-Z_]", "_", "all") />
<cfset local.bean = application._taffy.factory.getBean(local.currentResource.beanName) />
<cfset local.md = getMetaData(local.bean) />
<cfif structKeyExists(local.md, "taffy_dashboard_hide") OR structKeyExists(local.md, "taffy:dashboard:hide")>
<cfscript>continue;</cfscript>
</cfif>
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a href="###local.resourceHTTPID#" class="accordion-toggle" data-toggle="collapse" data-parent="##resourcesAccordion">
<cfif structKeyExists(local.md, "taffy:dashboard:name")>
#local.md['taffy:dashboard:name']#
<cfelseif structKeyExists(local.md, "taffy_dashboard_name")>
#local.md['taffy_dashboard_name']#
<cfelseif structKeyExists(local.md, "taffy:docs:name")>
#local.md['taffy:docs:name']#
<cfelseif structKeyExists(local.md, "taffy_docs_name")>
#local.md['taffy_docs_name']#
<cfelse>
#local.currentResource.beanName#
</cfif>
</a>
<cfloop list="DELETE|warning,PATCH|warning,PUT|warning,POST|danger,GET|primary" index="local.verb">
<cfif structKeyExists(local.currentResource.methods, listFirst(local.verb,'|'))>
<span class="verb label label-success">#ucase(listFirst(local.verb,'|'))#</span>
<cfelse>
<span class="verb label label-default">#ucase(listFirst(local.verb,'|'))#</span>
</cfif>
</cfloop>
<code class="resourceUriPath">#local.currentResource.srcUri#</code>
</h4>
</div>
<div class="panel-collapse collapse" id="#local.resourceHTTPID#">
<div class="panel-body resourceWrapper">
<div class="col-md-12">
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a aria-controls="settings" role="tab" data-toggle="tab" href="###local.resourceHTTPID#_run">Run it</a></li>
<li role="presentation"><a aria-controls="settings" role="tab" data-toggle="tab" href="###local.resourceHTTPID#_docs">Documentation</a></li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="#local.resourceHTTPID#_run">
<div class="runner">
<div class="well resource" data-uri="#local.currentResource.srcUri#" data-bean-name="#local.resourceHTTPID#">
<button class="btn btn-primary submitRequest">Send</button>
<button class="btn btn-success resetRequest">Reset</button>
<select class="form-control input-sm reqMethod">
<cfloop list="GET,POST,PUT,PATCH,DELETE" index="local.verb">
<cfif structKeyExists(local.currentResource.methods, local.verb)>
<option value="#local.verb#">#local.verb#</option>
</cfif>
</cfloop>
<cfif application._taffy.settings.allowCrossDomain NEQ 'false'>
<option value="OPTIONS">OPTIONS</option>
</cfif>
</select>
<input type="text" class="resourceUri form-control" value="#local.currentResource.srcUri#" onclick="this.select()" />
<div class="toggles">
<a class="expander" data-target="##qp_#local.resourceHTTPID#">+Query Params</a>
&nbsp;<a class="expander" data-target="##accept_#local.resourceHTTPID#">+Accept</a>
&nbsp;<a class="expander" data-target="##head_#local.resourceHTTPID#">+Headers</a>
&nbsp;<a class="expander" data-target="##auth_#local.resourceHTTPID#">+Basic Auth</a>
</div>
<div class="queryParams expandable" id="qp_#local.resourceHTTPID#">
<h4>Query String Parameters: <span class="text-muted">(optional)</span></h4>
<div class="qparam row form-group">
<div class="col-md-4">
<input class="form-control input-small paramName" />
</div>
<div class="col-md-1 micro">=</div>
<div class="col-md-4">
<input class="form-control input-small paramValue" />
</div>
<div class="col-md-2">
<button class="btn addParam" tabindex="-1">+</button>
</div>
</div>
</div>
<div class="expandable" id="accept_#local.resourceHTTPID#">
<h4>Accept:</h4>
<select class="form-control input-sm reqFormat">
<cfloop list="#structKeyList(application._taffy.settings.mimeTypes)#" index="local.mime">
<option value="#local.mime#"
<cfif application._taffy.settings.defaultMime eq application._taffy.settings.mimeTypes[local.mime]>selected="selected"</cfif>
>#application._taffy.settings.mimeTypes[local.mime]#</option>
</cfloop>
</select>
</div>
<cfif arrayLen(local.currentResource.tokens) gt 0>
<div class="reqTokens">
<h4>URI Tokens: <span class="text-muted">(required)</span></h4>
<div class='tokenErrors'></div>
<form class="form-horizontal" onsubmit="return false;">
<cfloop from="1" to="#arrayLen(local.currentResource.tokens)#" index="local.token">
<div class="form-group row">
<div class="col-md-3">
<label class="control-label" for="token_#local.resourceHTTPID#_#local.currentResource.tokens[local.token]#">#local.currentResource.tokens[local.token]#:</label>
</div>
<div class="col-md-6">
<input id="token_#local.resourceHTTPID#_#local.currentResource.tokens[local.token]#" name="#local.currentResource.tokens[local.token]#" type="text" class="form-control input-sm" />
</div>
</div>
</cfloop>
</form>
</div>
</cfif>
<div class="reqHeaders expandable" id="head_#local.resourceHTTPID#">
<h4>Request Headers:</h4>
<textarea
rows="#listLen(structKeyList(application._taffy.settings.dashboardHeaders, '|'), '|')+1#"
class="form-control input-sm requestHeaders"
placeholder="X-MY-HEADER: VALUE"
><cfloop list="#structKeyList(application._taffy.settings.dashboardHeaders, '|')#" delimiters="|" index="k">#k#: #application._taffy.settings.dashboardHeaders[k]##chr(13)##chr(10)#</cfloop></textarea>
</div>
<div class="expandable" id="auth_#local.resourceHTTPID#">
<h4>Basic Auth:</h4>
<div class="basicAuth row">
<div class="col-md-6"><input type="text" name="username" class="form-control" placeholder="Username" value="" /></div>
<div class="col-md-6"><input type="password" name="password" class="form-control" placeholder="Password" value="" /></div>
</div>
</div>
<div class="reqBody">
<h4>Request Body:</h4>
<textarea id="#local.resourceHTTPID#_RequestBody" class="form-control input-sm" rows="5"></textarea>
<cfif structKeyExists(local.md,"functions")>
<cfset local.functions = local.md.functions />
<cfelse>
<cfset local.functions = arrayNew(1) />
</cfif>
<!--- only save body templates for POST & PUT --->
<cfloop from="1" to="#arrayLen(local.functions)#" index="local.f">
<cfif local.functions[local.f].name eq "POST" or local.functions[local.f].name eq "PUT" or local.functions[local.f].name eq "PATCH">
<cfset local.args = {} />
<!--- get a list of all function arguments --->
<cfloop from="1" to="#arrayLen(local.functions[local.f].parameters)#" index="local.parm">
<cfset local.paramAttributes = local.functions[local.f].parameters[local.parm]>
<cfif structKeyExists(local.paramAttributes, "taffy_docs_hide") OR structKeyExists(local.paramAttributes, "taffy:docs:hide") OR structKeyExists(local.paramAttributes, "taffy_dashboard_hide") OR structKeyExists(local.paramAttributes, "taffy:dashboard:hide")>
<cfscript>continue;</cfscript>
</cfif>
<cfif not structKeyExists(local.paramAttributes,"type")>
<cfset local.args[local.paramAttributes.name] = '' />
<cfelseif local.paramAttributes.type eq 'struct'>
<cfset local.args[local.paramAttributes.name] = structNew() />
<cfelseif local.paramAttributes.type eq 'array'>
<cfset local.args[local.paramAttributes.name] = arrayNew(1) />
<cfelseif local.paramAttributes.type eq 'numeric'>
<cfset local.args[local.paramAttributes.name] = 0 />
<cfelseif local.paramAttributes.type eq 'boolean'>
<cfset local.args[local.paramAttributes.name] = true />
<cfelse>
<cfset local.args[local.paramAttributes.name] = '' />
</cfif>
</cfloop>
<!--- omit uri tokens --->
<cfloop from="1" to="#arrayLen(local.currentResource.tokens)#" index="local.token">
<cfset structDelete(local.args, local.currentResource.tokens[local.token]) />
</cfloop>
<!--- save to page JS for runtime reference --->
<script>
taffy.resources['#local.resourceHTTPID#'] = taffy.resources['#local.resourceHTTPID#'] || {};
taffy.resources['#local.resourceHTTPID#']['#lcase(local.functions[local.f].name)#'] = #serializeJson(local.args)#;
</script>
</cfif>
</cfloop>
</div>
<div class="progress progress-striped active">
<div class="progress-bar" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">
<span class="sr-only">Loading...</span>
</div>
</div>
<div class="response">
<hr />
<h4>Response:</h4>
<div class="responseHeaders"></div>
<p class="responseTime"></p>
<p class="label label-default responseStatus"></p>
<pre><code class="responseBody"></code></pre>
</div>
</div><!-- /well (resource) -->
</div><!-- /runner -->
</div>
<div role="tabpanel" class="tab-pane" id="#local.resourceHTTPID#_docs">
<cfset local.metadata = getMetaData(application._taffy.factory.getBean(local.currentResource.beanName)) />
<cfset local.docData = getHintsFromMetadata(local.metadata) />
<cfif structKeyExists(local.docData, 'hint')><div class="doc">#local.docData.hint#</div><hr/></cfif>
<cfset local.found = { get=false, post=false, put=false, patch=false, delete=false } />
<cfloop from="1" to="#arrayLen(local.docData.functions)#" index="local.f">
<cfset local.func = local.docData.functions[local.f] />
<cfset local.found[local.func.name] = true />
<!--- skip methods that are hidden --->
<cfif structKeyExists(local.func, "taffy_docs_hide") OR structKeyExists(local.func, "taffy:docs:hide") OR structKeyExists(local.func, "taffy_dashboard_hide") OR structKeyExists(local.func, "taffy:dashboard:hide")>
<cfscript>continue;</cfscript>
</cfif>
<!--- exclude methods that are not exposed as REST verbs --->
<cfif listFindNoCase('get,post,put,delete,patch',local.func.name) OR structKeyExists(local.func,'taffy_verb') OR structKeyExists(local.func,'taffy:verb')>
<div class="col-md-12">
<h5 class="verbHeading">
<cfif listFindNoCase('get,post,put,delete,patch',local.func.name)>
#local.func.name#
<cfelseif structKeyExists(local.func,'taffy_verb')>
#local.func.taffy_verb#
<cfelseif structKeyExists(local.func,'taffy:verb')>
#local.func['taffy:verb']#
</cfif>
</h5>
</div>
<cfif structKeyExists(local.func, "hint")>
<div class="col-md-12 doc">#local.func.hint#</div>
</cfif>
<div class="inputs-wrapper">
<div class="panel panel-default resource-docs">
<div class="panel-heading">
<h6 class="panel-title"><a href="###local.resourceHTTPID#_#local.func.name#_inputs" class="accordion-toggle" data-toggle="collapse" data-parent="###local.resourceHTTPID#_docs">Inputs</a></h6>
</div>
<div class="panel-collapse collapse" id="#local.resourceHTTPID#_#local.func.name#_inputs">
<div class="panel-body">
<cfloop from="1" to="#arrayLen(local.func.parameters)#" index="local.p">
<cfset local.param = local.func.parameters[local.p] />
<cfif structKeyExists(local.param, "taffy_docs_hide") OR structKeyExists(local.param, "taffy:docs:hide") OR structKeyExists(local.param, "taffy_dashboard_hide") OR structKeyExists(local.param, "taffy:dashboard:hide")>
<cfscript>continue;</cfscript>
</cfif>
<div class="row">
<div class="col-md-12">
<cfif not structKeyExists(local.param, 'required') or not local.param.required>
optional
<cfelse>
required
</cfif>
<cfif structKeyExists(local.param, "type")>
#local.param.type#
</cfif>
<strong>#local.param.name#</strong>
<cfif structKeyExists(local.param, "default")>
<cfif local.param.default eq "">
(default: "")
<cfelse>
(default: #local.param.default#)
</cfif>
<cfelse>
<!--- no default value --->
</cfif>
<cfif structKeyExists(local.param, "hint")>
<br/><p class="doc hint">#local.param.hint#</p>
</cfif>
</div>
</div>
</cfloop>
</div>
</div>
</div>
<!--- begin sample response --->
<cfset hasSample = false />
<cfset sample = '' />
<cfloop from="1" to="#arrayLen(local.md.functions)#" index="functionIndex">
<cfif local.md.functions[functionIndex].name eq 'sample#local.func.name#Response'>
<cfset hasSample = true />
<cfinvoke
component="#local.bean#"
method="#local.md.functions[functionIndex].name#"
returnvariable="sample"
/>
<cfbreak />
</cfif>
</cfloop>
<cfif hasSample>
<div class="panel panel-default resource-docs">
<div class="panel-heading">
<h6 class="panel-title">
<a href="###local.resourceHTTPID#_#local.func.name#_sample" class="accordion-toggle" data-toggle="collapse" data-parent="###local.resourceHTTPID#_docs">Sample Response</a>
</h6>
</div>
<div class="panel-collapse collapse" id="#local.resourceHTTPID#_#local.func.name#_sample">
<div class="panel-body">
<div class="col-md-12">
<script type="text/javascript" defer>
document.write("<pre><code>");
document.write(JSON.stringify(#serializeJson(sample)#, null, ' '));
document.write("</code></pre>");
</script>
</div>
</div>
</div>
</div>
</cfif>
<!--- end sample response --->
</div>
</cfif>
</cfloop>
</div>
</div>
</div>
</div>
</div>
</div>
</cfloop>
</cfoutput>
</div><!-- /panel-group -->
<br />
<cfif arrayLen(application._taffy.uriMatchOrder) eq 0>
<div class="panel panel-warning">
<div class="panel-heading">Taffy is running but you haven't defined any resources yet.</div>
<div class="panel-body">
<p>
It looks like you don't have any resources defined. Get started by creating the folder
<code><cfoutput>#guessResourcesFullPath()#</cfoutput></code>, in which you should place your
Resource CFC's.
</p>
<p>
Or you could set up a bean factory, like <a href="http://www.coldspringframework.org/">ColdSpring</a>
or <a href="https://github.com/seancorfield/di1">DI/1</a>. Want to know more about using bean factories with Taffy?
<a href="https://github.com/atuttle/Taffy/wiki/So-you-want-to:-use-an-external-bean-factory-like-coldspring-to-completely-manage-resources"
>Check out the wiki!</a>
</p>
<p>
If all else fails, I recommend starting with <a href="https://github.com/atuttle/Taffy/wiki/Getting-Started">Getting Started</a>.
</p>
</div>
</div>
</cfif>
<cfif application._taffy.settings.reloadKey eq "reload" and application._taffy.settings.reloadPassword eq "true">
<div class="alert alert-warning">
<strong>Warning:</strong> Your reload key and password are using the framework default settings.
It's advised that you <a target="_blank" rel="noreferrer noopener" href="<cfoutput>#getDocUrl('reloadKey')#</cfoutput>">change these in production</a>.
</div>
</cfif>
<div class="alert alert-info">Resources are listed in matching order. From top to bottom, the first URI to match the request is used.</div>
</div><!-- /#resources -->
</div><!-- /container -->
<script type="text/javascript">
<cfinclude template="jquery.min.js" />
<cfinclude template="bootstrap.min.js" />
<cfinclude template="highlight.min.js" />
<cfinclude template="dash.js" />
$(function(){
hljs.initHighlighting();
var baseurl = '<cfoutput>#cgi.script_name#?dashboard</cfoutput>';
$("#reload").click(function(){
var reloadUrl = baseurl + '<cfoutput>&#application._taffy.settings.reloadKey#=#application._taffy.settings.reloadPassword#</cfoutput>';
var btn = $("#reload");
btn.html('Reloading...').attr('disabled','disabled');
$.ajax({
url: reloadUrl
,type: 'GET'
,cache: false
}).done(function(data){
//notify reload success
$("#alerts").append('<div class="alert alert-success" id="reloadSuccess">API Cache Successfully Reloaded. Refresh to see changes.</div>');
btn.removeAttr('disabled').html('Reload API Cache');
setTimeout(function(){
$("#reloadSuccess").fadeOut('fast', function(){
$(this).remove();
});
}, 2000);
}).fail(function(jqxhr, status, error){
//notify reload fail
$("#alerts").append('<div class="alert alert-danger" id="reloadFail">API Cache Reload Failed!</div>');
btn.removeAttr('disabled').html('Reload API Cache');
setTimeout(function(){
$("#reloadFail").fadeOut('fast', function(){
$(this).remove();
});
}, 2000);
});
});
$(".hideDocs").on("click", function(){
var docs = $(this).closest('.docs');
var runner = $(this).closest('.resourceWrapper').find('.runner');
docs.hide();
runner.removeClass("col-md-8").addClass("col-md-12").find('.showDocs').show();
});
$(".showDocs").on("click", function(){
var docs = $(this).closest('.resourceWrapper').find('.docs');
var runner = $(this).closest('.runner');
runner.removeClass("col-md-12").addClass("col-md-8");
docs.show();
$(this).hide();
}).each(function(){
$(this).click();
});
});
function submitRequest( verb, resource, headers, body, callback ){
var url = window.location.protocol + '//' + window.location.host;
var endpointURLParam = '<cfoutput>#jsStringFormat(application._taffy.settings.endpointURLParam)#</cfoutput>';
var endpoint = resource.split('?')[0];
var args = '';
var dType = null;
<cfif Len(application._taffy.settings.csrfToken.cookieName) AND Len(application._taffy.settings.csrfToken.headerName)>
<cfif structKeyExists(GetFunctionList(), "encodeForJavascript")>
<cfset local.csrfCookieName = encodeForJavascript(application._taffy.settings.csrfToken.cookieName)>
<cfset local.csrfHeaderName = encodeForJavascript(application._taffy.settings.csrfToken.headerName)>
<cfelse>
<cfset local.csrfCookieName = jsStringFormat(application._taffy.settings.csrfToken.cookieName)>
<cfset local.csrfHeaderName = jsStringFormat(application._taffy.settings.csrfToken.headerName)>
</cfif>
var csrfCookie = getCookie('<cfoutput>#local.csrfCookieName#</cfoutput>');
if (csrfCookie) {
headers['<cfoutput>#local.csrfHeaderName#</cfoutput>'] = csrfCookie;
}
</cfif>
url += '<cfoutput>#cgi.SCRIPT_NAME#</cfoutput>' + '?' + endpointURLParam + '=' + encodeURIComponent(endpoint);
if( resource.indexOf('?') && resource.split('?')[1] ){
url += '&' + resource.split('?')[1];
}
if( body && typeof body === 'string' ){
try {
JSON.parse(body);
dType = "application/json";
} catch (e) {
//Not a valid JSON string
}
}
var before = Date.now();
$.ajax({
type: verb
,url: url
,cache: false
,headers: headers
,data: body
,contentType: dType
}).always(function(a,b,c){
var after = Date.now(), t = after-before;
var xhr = (a && a.getAllResponseHeaders) ? a : c;
callback(
t
, xhr.status + " " + xhr.statusText //status
, xhr.getAllResponseHeaders() //headers
, xhr.responseText //body
);
});
}
function getCookie(name) {
var nameEQ = name + '=', ca = document.cookie.split(';'), i = 0, c;
for(;i < ca.length;i++) {
c = ca[i];
while (c[0]==' ') c = c.substring(1);
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length);
}
return null;
}
function filterResources(){
var filter = $('#resourceSearch').val().toUpperCase();
var ul = $('#resourcesAccordion');
var li = ul.find('.panel').not('.resource-docs');
li.each(function(){
var row = $(this);
var rowText = $(row.find('a')[0]).text();
if ( rowText.toUpperCase().indexOf(filter) > -1 ){
row.css({ display: '' });
}else{
row.css({ display: 'none' });
}
});
}
function clearSearch(evt, input) {
var code = evt.charCode || evt.keyCode;
if (code == 27) { input.value = '';}
}
document.getElementById("resourceSearch").addEventListener("keyup", filterResources);
document.getElementById("resourceSearch").addEventListener("keydown", function(e){
clearSearch(e, this);
});
</script>
</body>
</html>
<cffunction name="getDocUrl">
<cfargument name="item" />
<cfreturn "https://docs.taffy.io/##/#listFirst(application._taffy.version,'-')#?id=#lCase(item)#" />
</cffunction>
+220
View File
@@ -0,0 +1,220 @@
<!DOCTYPE HTML>
<html>
<head>
<title><cfoutput>#application._taffy.settings.docs.APIName# Documentation - #application._taffy.settings.docs.APIversion#</cfoutput></title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="https://fav.farm/🍬" />
<style>
<cfinclude template="dash.css" />
<cfinclude template="highlight-github.min.css" />
</style>
</head>
<body>
<script>
window.taffy = { resources: {} };
</script>
<div class="container">
<div class="masthead">
<h1><cfoutput>#application._taffy.settings.docs.APIName#</cfoutput></h1>
<span class="ver text-muted" style="left: 0">Version <cfoutput>#application._taffy.settings.docs.APIversion#</cfoutput></span>
</div>
<div class="row" id="resources">
<h3>Resources:</h3>
<div class="panel-group" id="resourcesAccordion">
<cfoutput>
<cfloop from="1" to="#arrayLen(application._taffy.uriMatchOrder)#" index="local.resource">
<cfset local.currentResource = application._taffy.endpoints[application._taffy.uriMatchOrder[local.resource]] />
<cfset local.bean = application._taffy.factory.getBean(local.currentResource.beanName) />
<cfset local.beanMeta = getMetaData(local.bean) />
<cfif structKeyExists(local.beanMeta, "taffy_docs_hide") OR structKeyExists(local.beanMeta, "taffy:docs:hide")>
<cfscript>continue;</cfscript>
</cfif>
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a href="###local.currentResource.beanName#" class="accordion-toggle" data-toggle="collapse" data-parent="##resourcesAccordion">
<cfif structKeyExists(local.beanMeta, "taffy:docs:name")>
#local.beanMeta['taffy:docs:name']#
<cfelseif structKeyExists(local.beanMeta, "taffy_docs_name")>
#local.beanMeta['taffy_docs_name']#
<cfelse>
#local.currentResource.beanName#
</cfif>
</a>
<cfloop list="DELETE|warning,PATCH|warning,PUT|warning,POST|danger,GET|primary" index="local.verb">
<cfif structKeyExists(local.currentResource.methods, listFirst(local.verb,'|'))>
<span class="verb label label-success">#ucase(listFirst(local.verb,'|'))#</span>
</cfif>
</cfloop>
<code style="float:right; margin-top: -15px; display: inline-block;">#local.currentResource.srcUri#</code>
</h4>
</div>
<div id="#local.currentResource.beanName#" class="in">
<div class="panel-body resourceWrapper">
<div class="col-md-12 docs">
<cfset local.metadata = getMetaData(application._taffy.factory.getBean(local.currentResource.beanName)) />
<cfset local.docData = getHintsFromMetadata(local.metadata) />
<cfif structKeyExists(local.docData, 'hint')><div class="doc">#docData.hint#</div><hr/></cfif>
<cfset local.found = { get=false, post=false, put=false, patch=false, delete=false } />
<cfloop from="1" to="#arrayLen(local.docData.functions)#" index="local.f">
<cfset local.func = local.docData.functions[local.f] />
<cfset verbs = "GET,POST,PUT,PATCH,DELETE,OPTIONS,HEAD" />
<cfset thisVerb = local.func.name />
<cfif structKeyExists(local.func,"taffy_verb")>
<cfset thisVerb = local.func.taffy_verb />
<cfelseif structKeyExists(local.func,"taffy:verb")>
<cfset thisVerb = local.func['taffy:verb'] />
</cfif>
<cfif listFindNoCase(verbs, thisVerb) eq 0
OR structKeyExists(local.func, "taffy_docs_hide")
OR structKeyExists(local.func, "taffy:docs:hide")>
<cfscript>continue;</cfscript><!--- this has to be script for CF8 compat --->
</cfif>
<cfset local.found[local.func.name] = true />
<div class="col-md-12"><h5 class="verbHeading">#thisVerb#</h5></div>
<cfif structKeyExists(local.func, "hint")>
<div class="col-md-12 doc">#local.func.hint#</div>
</cfif>
<div class="inputs-wrapper">
<!--- begin inputs panel --->
<cfset inputsCount = 0 />
<cfloop from="1" to="#arrayLen(local.func.parameters)#" index="local.p">
<cfset local.param = local.func.parameters[local.p] />
<cfif structKeyExists(local.param, "taffy_docs_hide") OR structKeyExists(local.param, "taffy:docs:hide")>
<cfscript>continue;</cfscript>
</cfif>
<cfset inputsCount++ />
</cfloop>
<cfif inputsCount gt 0>
<div class="panel panel-default">
<div class="panel-heading">
<h6 class="panel-title"><a href="###local.currentResource.beanName#_#local.func.name#_inputs" class="accordion-toggle" data-toggle="collapse" data-parent="###local.currentResource.beanName#_docs">Inputs</a></h6>
</div>
<div class="panel-collapse collapse" id="#local.currentResource.beanName#_#local.func.name#_inputs">
<div class="panel-body">
<cfloop from="1" to="#arrayLen(local.func.parameters)#" index="local.p">
<cfset local.param = local.func.parameters[local.p] />
<cfif structKeyExists(local.param, "taffy_docs_hide") OR structKeyExists(local.param, "taffy:docs:hide")>
<cfscript>continue;</cfscript>
</cfif>
<div class="row">
<div class="col-md-12">
<strong>#local.param.name#</strong>
<cfif not structKeyExists(local.param, 'required') or not local.param.required>
optional
<cfelse>
required
</cfif>
<cfif structKeyExists(local.param, "type")>
#local.param.type#
</cfif>
<cfif structKeyExists(local.param, "default")>
<cfif local.param.default eq "">
(default: "")
<cfelse>
(default: #local.param.default#)
</cfif>
<cfelse>
<!--- no default value --->
</cfif>
<cfif structKeyExists(local.param, "hint")>
<br/><p class="doc hint">#local.param.hint#</p>
</cfif>
</div>
</div>
</cfloop>
</div>
</div>
</div>
</cfif>
<!--- end inputs panel --->
<!--- begin sample response --->
<cfset hasSample = false />
<cfset sample = '' />
<cfloop from="1" to="#arrayLen(local.beanMeta.functions)#" index="functionIndex">
<cfif local.beanMeta.functions[functionIndex].name eq 'sample#local.func.name#Response'>
<cfset hasSample = true />
<cfinvoke
component="#local.bean#"
method="sample#local.func.name#Response"
argumentcollection={}
returnvariable="sample"
/>
<cfbreak />
</cfif>
</cfloop>
<cfif hasSample>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<h6 class="panel-title">
<a href="###local.currentResource.beanName#_#local.func.name#_sample" class="accordion-toggle" data-toggle="collapse" data-parent="###local.currentResource.beanName#_docs">Sample Response</a>
</h6>
</div>
<div class="panel-collapse collapse" id="#local.currentResource.beanName#_#local.func.name#_sample">
<div class="panel-body">
<div class="col-md-12">
<script type="text/javascript" defer>
document.write("<pre><code>");
document.write(JSON.stringify(#serializeJson(sample)#, null, ' '));
document.write("</code></pre>");
</script>
</div>
</div>
</div>
</div>
</div>
</div>
</cfif>
<!--- end sample response --->
</div>
</cfloop>
</div><!-- /col-md-6 (docs) -->
</div>
</div>
</div>
</cfloop>
</cfoutput>
</div><!-- /panel-group -->
<br />
<cfif arrayLen(application._taffy.uriMatchOrder) eq 0>
<div class="panel panel-warning">
<div class="panel-heading">Taffy is running but you haven't defined any resources yet.</div>
<div class="panel-body">
<p>
It looks like you don't have any resources defined. Get started by creating the folder
<code><cfoutput>#guessResourcesFullPath()#</cfoutput></code>, in which you should place your
Resource CFC's.
</p>
<p>
Or you could set up a bean factory, like <a href="http://www.coldspringframework.org/">ColdSpring</a>
or <a href="https://github.com/seancorfield/di1">DI/1</a>. Want to know more about using bean factories with Taffy?
<a href="https://github.com/atuttle/Taffy/wiki/So-you-want-to:-use-an-external-bean-factory-like-coldspring-to-completely-manage-resources"
>Check out the wiki!</a>
</p>
<p>
If all else fails, I recommend starting with <a href="https://github.com/atuttle/Taffy/wiki/Getting-Started">Getting Started</a>.
</p>
</div>
</div>
</cfif>
<div class="alert alert-info">Resources are listed in matching order. From top to bottom, the first URI to match the request is used.</div>
</div><!-- /#resources -->
</div><!-- /container -->
<script type="text/javascript">
<cfinclude template="jquery.min.js" />
<cfinclude template="bootstrap.min.js" />
<cfinclude template="highlight.min.js" />
<cfinclude template="dash.js" />
$(function(){
hljs.initHighlighting();
});
</script>
</body>
</html>
+31
View File
@@ -0,0 +1,31 @@
/*
github.com style (c) Vasily Polovnyov <vast@whiteants.net>
highlight.js v8.2
Copyright (c) 2006, Ivan Sagalaev
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of highlight.js nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
.hljs{display:block;overflow-x:auto;padding:.5em;color:#333;background:#f8f8f8;-webkit-text-size-adjust:none}.hljs-comment,.hljs-template_comment,.diff .hljs-header,.hljs-javadoc{color:#998;font-style:italic}.hljs-keyword,.css .rule .hljs-keyword,.hljs-winutils,.javascript .hljs-title,.nginx .hljs-title,.hljs-subst,.hljs-request,.hljs-status{color:#333;font-weight:bold}.hljs-number,.hljs-hexcolor,.ruby .hljs-constant{color:#008080}.hljs-string,.hljs-tag .hljs-value,.hljs-phpdoc,.hljs-dartdoc,.tex .hljs-formula{color:#d14}.hljs-title,.hljs-id,.scss .hljs-preprocessor{color:#900;font-weight:bold}.javascript .hljs-title,.hljs-list .hljs-keyword,.hljs-subst{font-weight:normal}.hljs-class .hljs-title,.hljs-type,.vhdl .hljs-literal,.tex .hljs-command{color:#458;font-weight:bold}.hljs-tag,.hljs-tag .hljs-title,.hljs-rules .hljs-property,.django .hljs-tag .hljs-keyword{color:#000080;font-weight:normal}.hljs-attribute,.hljs-variable,.lisp .hljs-body{color:#008080}.hljs-regexp{color:#009926}.hljs-symbol,.ruby .hljs-symbol .hljs-string,.lisp .hljs-keyword,.clojure .hljs-keyword,.scheme .hljs-keyword,.tex .hljs-special,.hljs-prompt{color:#990073}.hljs-built_in{color:#0086b3}.hljs-preprocessor,.hljs-pragma,.hljs-pi,.hljs-doctype,.hljs-shebang,.hljs-cdata{color:#999;font-weight:bold}.hljs-deletion{background:#fdd}.hljs-addition{background:#dfd}.diff .hljs-change{background:#0086b3}.hljs-chunk{color:#aaa}
+29
View File
@@ -0,0 +1,29 @@
/*
highlight.js v8.2
Copyright (c) 2006, Ivan Sagalaev
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of highlight.js nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
var hljs=new function(){function j(v){return v.replace(/&/gm,"&amp;").replace(/</gm,"&lt;").replace(/>/gm,"&gt;")}function t(v){return v.nodeName.toLowerCase()}function h(w,x){var v=w&&w.exec(x);return v&&v.index==0}function r(w){var v=(w.className+" "+(w.parentNode?w.parentNode.className:"")).split(/\s+/);v=v.map(function(x){return x.replace(/^lang(uage)?-/,"")});return v.filter(function(x){return i(x)||/no(-?)highlight/.test(x)})[0]}function o(x,y){var v={};for(var w in x){v[w]=x[w]}if(y){for(var w in y){v[w]=y[w]}}return v}function u(x){var v=[];(function w(y,z){for(var A=y.firstChild;A;A=A.nextSibling){if(A.nodeType==3){z+=A.nodeValue.length}else{if(A.nodeType==1){v.push({event:"start",offset:z,node:A});z=w(A,z);if(!t(A).match(/br|hr|img|input/)){v.push({event:"stop",offset:z,node:A})}}}}return z})(x,0);return v}function q(w,y,C){var x=0;var F="";var z=[];function B(){if(!w.length||!y.length){return w.length?w:y}if(w[0].offset!=y[0].offset){return(w[0].offset<y[0].offset)?w:y}return y[0].event=="start"?w:y}function A(H){function G(I){return" "+I.nodeName+'="'+j(I.value)+'"'}F+="<"+t(H)+Array.prototype.map.call(H.attributes,G).join("")+">"}function E(G){F+="</"+t(G)+">"}function v(G){(G.event=="start"?A:E)(G.node)}while(w.length||y.length){var D=B();F+=j(C.substr(x,D[0].offset-x));x=D[0].offset;if(D==w){z.reverse().forEach(E);do{v(D.splice(0,1)[0]);D=B()}while(D==w&&D.length&&D[0].offset==x);z.reverse().forEach(A)}else{if(D[0].event=="start"){z.push(D[0].node)}else{z.pop()}v(D.splice(0,1)[0])}}return F+j(C.substr(x))}function m(y){function v(z){return(z&&z.source)||z}function w(A,z){return RegExp(v(A),"m"+(y.cI?"i":"")+(z?"g":""))}function x(D,C){if(D.compiled){return}D.compiled=true;D.k=D.k||D.bK;if(D.k){var z={};var E=function(G,F){if(y.cI){F=F.toLowerCase()}F.split(" ").forEach(function(H){var I=H.split("|");z[I[0]]=[G,I[1]?Number(I[1]):1]})};if(typeof D.k=="string"){E("keyword",D.k)}else{Object.keys(D.k).forEach(function(F){E(F,D.k[F])})}D.k=z}D.lR=w(D.l||/\b[A-Za-z0-9_]+\b/,true);if(C){if(D.bK){D.b="\\b("+D.bK.split(" ").join("|")+")\\b"}if(!D.b){D.b=/\B|\b/}D.bR=w(D.b);if(!D.e&&!D.eW){D.e=/\B|\b/}if(D.e){D.eR=w(D.e)}D.tE=v(D.e)||"";if(D.eW&&C.tE){D.tE+=(D.e?"|":"")+C.tE}}if(D.i){D.iR=w(D.i)}if(D.r===undefined){D.r=1}if(!D.c){D.c=[]}var B=[];D.c.forEach(function(F){if(F.v){F.v.forEach(function(G){B.push(o(F,G))})}else{B.push(F=="self"?D:F)}});D.c=B;D.c.forEach(function(F){x(F,D)});if(D.starts){x(D.starts,C)}var A=D.c.map(function(F){return F.bK?"\\.?("+F.b+")\\.?":F.b}).concat([D.tE,D.i]).map(v).filter(Boolean);D.t=A.length?w(A.join("|"),true):{exec:function(F){return null}}}x(y)}function c(T,L,J,R){function v(V,W){for(var U=0;U<W.c.length;U++){if(h(W.c[U].bR,V)){return W.c[U]}}}function z(V,U){if(h(V.eR,U)){return V}if(V.eW){return z(V.parent,U)}}function A(U,V){return !J&&h(V.iR,U)}function E(W,U){var V=M.cI?U[0].toLowerCase():U[0];return W.k.hasOwnProperty(V)&&W.k[V]}function w(aa,Y,X,W){var U=W?"":b.classPrefix,V='<span class="'+U,Z=X?"":"</span>";V+=aa+'">';return V+Y+Z}function N(){if(!I.k){return j(C)}var U="";var X=0;I.lR.lastIndex=0;var V=I.lR.exec(C);while(V){U+=j(C.substr(X,V.index-X));var W=E(I,V);if(W){H+=W[1];U+=w(W[0],j(V[0]))}else{U+=j(V[0])}X=I.lR.lastIndex;V=I.lR.exec(C)}return U+j(C.substr(X))}function F(){if(I.sL&&!f[I.sL]){return j(C)}var U=I.sL?c(I.sL,C,true,S):e(C);if(I.r>0){H+=U.r}if(I.subLanguageMode=="continuous"){S=U.top}return w(U.language,U.value,false,true)}function Q(){return I.sL!==undefined?F():N()}function P(W,V){var U=W.cN?w(W.cN,"",true):"";if(W.rB){D+=U;C=""}else{if(W.eB){D+=j(V)+U;C=""}else{D+=U;C=V}}I=Object.create(W,{parent:{value:I}})}function G(U,Y){C+=U;if(Y===undefined){D+=Q();return 0}var W=v(Y,I);if(W){D+=Q();P(W,Y);return W.rB?0:Y.length}var X=z(I,Y);if(X){var V=I;if(!(V.rE||V.eE)){C+=Y}D+=Q();do{if(I.cN){D+="</span>"}H+=I.r;I=I.parent}while(I!=X.parent);if(V.eE){D+=j(Y)}C="";if(X.starts){P(X.starts,"")}return V.rE?0:Y.length}if(A(Y,I)){throw new Error('Illegal lexeme "'+Y+'" for mode "'+(I.cN||"<unnamed>")+'"')}C+=Y;return Y.length||1}var M=i(T);if(!M){throw new Error('Unknown language: "'+T+'"')}m(M);var I=R||M;var S;var D="";for(var K=I;K!=M;K=K.parent){if(K.cN){D=w(K.cN,"",true)+D}}var C="";var H=0;try{var B,y,x=0;while(true){I.t.lastIndex=x;B=I.t.exec(L);if(!B){break}y=G(L.substr(x,B.index-x),B[0]);x=B.index+y}G(L.substr(x));for(var K=I;K.parent;K=K.parent){if(K.cN){D+="</span>"}}return{r:H,value:D,language:T,top:I}}catch(O){if(O.message.indexOf("Illegal")!=-1){return{r:0,value:j(L)}}else{throw O}}}function e(y,x){x=x||b.languages||Object.keys(f);var v={r:0,value:j(y)};var w=v;x.forEach(function(z){if(!i(z)){return}var A=c(z,y,false);A.language=z;if(A.r>w.r){w=A}if(A.r>v.r){w=v;v=A}});if(w.language){v.second_best=w}return v}function g(v){if(b.tabReplace){v=v.replace(/^((<[^>]+>|\t)+)/gm,function(w,z,y,x){return z.replace(/\t/g,b.tabReplace)})}if(b.useBR){v=v.replace(/\n/g,"<br>")}return v}function p(A){var B=r(A);if(/no(-?)highlight/.test(B)){return}var y;if(b.useBR){y=document.createElementNS("http://www.w3.org/1999/xhtml","div");y.innerHTML=A.innerHTML.replace(/\n/g,"").replace(/<br[ \/]*>/g,"\n")}else{y=A}var z=y.textContent;var v=B?c(B,z,true):e(z);var x=u(y);if(x.length){var w=document.createElementNS("http://www.w3.org/1999/xhtml","div");w.innerHTML=v.value;v.value=q(x,u(w),z)}v.value=g(v.value);A.innerHTML=v.value;A.className+=" hljs "+(!B&&v.language||"");A.result={language:v.language,re:v.r};if(v.second_best){A.second_best={language:v.second_best.language,re:v.second_best.r}}}var b={classPrefix:"hljs-",tabReplace:null,useBR:false,languages:undefined};function s(v){b=o(b,v)}function l(){if(l.called){return}l.called=true;var v=document.querySelectorAll("pre code");Array.prototype.forEach.call(v,p)}function a(){addEventListener("DOMContentLoaded",l,false);addEventListener("load",l,false)}var f={};var n={};function d(v,x){var w=f[v]=x(this);if(w.aliases){w.aliases.forEach(function(y){n[y]=v})}}function k(){return Object.keys(f)}function i(v){return f[v]||f[n[v]]}this.highlight=c;this.highlightAuto=e;this.fixMarkup=g;this.highlightBlock=p;this.configure=s;this.initHighlighting=l;this.initHighlightingOnLoad=a;this.registerLanguage=d;this.listLanguages=k;this.getLanguage=i;this.inherit=o;this.IR="[a-zA-Z][a-zA-Z0-9_]*";this.UIR="[a-zA-Z_][a-zA-Z0-9_]*";this.NR="\\b\\d+(\\.\\d+)?";this.CNR="(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)";this.BNR="\\b(0b[01]+)";this.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~";this.BE={b:"\\\\[\\s\\S]",r:0};this.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[this.BE]};this.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[this.BE]};this.PWM={b:/\b(a|an|the|are|I|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such)\b/};this.CLCM={cN:"comment",b:"//",e:"$",c:[this.PWM]};this.CBCM={cN:"comment",b:"/\\*",e:"\\*/",c:[this.PWM]};this.HCM={cN:"comment",b:"#",e:"$",c:[this.PWM]};this.NM={cN:"number",b:this.NR,r:0};this.CNM={cN:"number",b:this.CNR,r:0};this.BNM={cN:"number",b:this.BNR,r:0};this.CSSNM={cN:"number",b:this.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0};this.RM={cN:"regexp",b:/\//,e:/\/[gim]*/,i:/\n/,c:[this.BE,{b:/\[/,e:/\]/,r:0,c:[this.BE]}]};this.TM={cN:"title",b:this.IR,r:0};this.UTM={cN:"title",b:this.UIR,r:0}}();hljs.registerLanguage("coffeescript",function(c){var b={keyword:"in if for while finally new do return else break catch instanceof throw try this switch continue typeof delete debugger super then unless until loop of by when and or is isnt not",literal:"true false null undefined yes no on off",reserved:"case default function var void with const let enum export import native __hasProp __extends __slice __bind __indexOf",built_in:"npm require console print module global window document"};var a="[A-Za-z$_][0-9A-Za-z$_]*";var f=c.inherit(c.TM,{b:a});var e={cN:"subst",b:/#\{/,e:/}/,k:b};var d=[c.BNM,c.inherit(c.CNM,{starts:{e:"(\\s*/)?",r:0}}),{cN:"string",v:[{b:/'''/,e:/'''/,c:[c.BE]},{b:/'/,e:/'/,c:[c.BE]},{b:/"""/,e:/"""/,c:[c.BE,e]},{b:/"/,e:/"/,c:[c.BE,e]}]},{cN:"regexp",v:[{b:"///",e:"///",c:[e,c.HCM]},{b:"//[gim]*",r:0},{b:/\/(?![ *])(\\\/|.)*?\/[gim]*(?=\W|$)/}]},{cN:"property",b:"@"+a},{b:"`",e:"`",eB:true,eE:true,sL:"javascript"}];e.c=d;return{aliases:["coffee","cson","iced"],k:b,i:/\/\*/,c:d.concat([{cN:"comment",b:"###",e:"###"},c.HCM,{cN:"function",b:"(^\\s*|\\B)("+a+"\\s*=\\s*)?(\\(.*\\))?\\s*\\B[-=]>",e:"[-=]>",rB:true,c:[f,{cN:"params",b:"\\([^\\(]",rB:true,c:[{b:/\(/,e:/\)/,k:b,c:["self"].concat(d)}]}]},{cN:"class",bK:"class",e:"$",i:/[:="\[\]]/,c:[{bK:"extends",eW:true,i:/[:="\[\]]/,c:[f]},f]},{cN:"attribute",b:a+":",e:":",rB:true,eE:true,r:0}])}});hljs.registerLanguage("nginx",function(c){var b={cN:"variable",v:[{b:/\$\d+/},{b:/\$\{/,e:/}/},{b:"[\\$\\@]"+c.UIR}]};var a={eW:true,l:"[a-z/_]+",k:{built_in:"on off yes no true false none blocked debug info notice warn error crit select break last permanent redirect kqueue rtsig epoll poll /dev/poll"},r:0,i:"=>",c:[c.HCM,{cN:"string",c:[c.BE,b],v:[{b:/"/,e:/"/},{b:/'/,e:/'/}]},{cN:"url",b:"([a-z]+):/",e:"\\s",eW:true,eE:true,c:[b]},{cN:"regexp",c:[c.BE,b],v:[{b:"\\s\\^",e:"\\s|{|;",rE:true},{b:"~\\*?\\s+",e:"\\s|{|;",rE:true},{b:"\\*(\\.[a-z\\-]+)+"},{b:"([a-z\\-]+\\.)+\\*"}]},{cN:"number",b:"\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}(:\\d{1,5})?\\b"},{cN:"number",b:"\\b\\d+[kKmMgGdshdwy]*\\b",r:0},b]};return{aliases:["nginxconf"],c:[c.HCM,{b:c.UIR+"\\s",e:";|{",rB:true,c:[{cN:"title",b:c.UIR,starts:a}],r:0}],i:"[^\\s\\}]"}});hljs.registerLanguage("json",function(a){var e={literal:"true false null"};var d=[a.QSM,a.CNM];var c={cN:"value",e:",",eW:true,eE:true,c:d,k:e};var b={b:"{",e:"}",c:[{cN:"attribute",b:'\\s*"',e:'"\\s*:\\s*',eB:true,eE:true,c:[a.BE],i:"\\n",starts:c}],i:"\\S"};var f={b:"\\[",e:"\\]",c:[a.inherit(c,{cN:null})],i:"\\S"};d.splice(d.length,0,b,f);return{c:d,k:e,i:"\\S"}});hljs.registerLanguage("http",function(a){return{i:"\\S",c:[{cN:"status",b:"^HTTP/[0-9\\.]+",e:"$",c:[{cN:"number",b:"\\b\\d{3}\\b"}]},{cN:"request",b:"^[A-Z]+ (.*?) HTTP/[0-9\\.]+$",rB:true,e:"$",c:[{cN:"string",b:" ",e:" ",eB:true,eE:true}]},{cN:"attribute",b:"^\\w",e:": ",eE:true,i:"\\n|\\s|=",starts:{cN:"string",e:"$"}},{b:"\\n\\n",starts:{sL:"",eW:true}}]}});hljs.registerLanguage("javascript",function(a){return{aliases:["js"],k:{keyword:"in if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const class",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document"},c:[{cN:"pi",b:/^\s*('|")use strict('|")/,r:10},a.ASM,a.QSM,a.CLCM,a.CBCM,a.CNM,{b:"("+a.RSR+"|\\b(case|return|throw)\\b)\\s*",k:"return throw case",c:[a.CLCM,a.CBCM,a.RM,{b:/</,e:/>;/,r:0,sL:"xml"}],r:0},{cN:"function",bK:"function",e:/\{/,eE:true,c:[a.inherit(a.TM,{b:/[A-Za-z$_][0-9A-Za-z$_]*/}),{cN:"params",b:/\(/,e:/\)/,c:[a.CLCM,a.CBCM],i:/["'\(]/}],i:/\[|%/},{b:/\$[(.]/},{b:"\\."+a.IR,r:0}]}});hljs.registerLanguage("sql",function(a){var b={cN:"comment",b:"--",e:"$"};return{cI:true,i:/[<>]/,c:[{cN:"operator",bK:"begin end start commit rollback savepoint lock alter create drop rename call delete do handler insert load replace select truncate update set show pragma grant merge describe use explain help declare prepare execute deallocate savepoint release unlock purge reset change stop analyze cache flush optimize repair kill install uninstall checksum restore check backup",e:/;/,eW:true,k:{keyword:"abs absolute acos action add adddate addtime aes_decrypt aes_encrypt after aggregate all allocate alter analyze and any are as asc ascii asin assertion at atan atan2 atn2 authorization authors avg backup before begin benchmark between bin binlog bit_and bit_count bit_length bit_or bit_xor both by cache call cascade cascaded case cast catalog ceil ceiling chain change changed char_length character_length charindex charset check checksum checksum_agg choose close coalesce coercibility collate collation collationproperty column columns columns_updated commit compress concat concat_ws concurrent connect connection connection_id consistent constraint constraints continue contributors conv convert convert_tz corresponding cos cot count count_big crc32 create cross cume_dist curdate current current_date current_time current_timestamp current_user cursor curtime data database databases datalength date_add date_format date_sub dateadd datediff datefromparts datename datepart datetime2fromparts datetimeoffsetfromparts day dayname dayofmonth dayofweek dayofyear deallocate declare decode default deferrable deferred degrees delayed delete des_decrypt des_encrypt des_key_file desc describe descriptor diagnostics difference disconnect distinct distinctrow div do domain double drop dumpfile each else elt enclosed encode encrypt end end-exec engine engines eomonth errors escape escaped event eventdata events except exception exec execute exists exp explain export_set extended external extract fast fetch field fields find_in_set first first_value floor flush for force foreign format found found_rows from from_base64 from_days from_unixtime full function get get_format get_lock getdate getutcdate global go goto grant grants greatest group group_concat grouping grouping_id gtid_subset gtid_subtract handler having help hex high_priority hosts hour ident_current ident_incr ident_seed identified identity if ifnull ignore iif ilike immediate in index indicator inet6_aton inet6_ntoa inet_aton inet_ntoa infile initially inner innodb input insert install instr intersect into is is_free_lock is_ipv4 is_ipv4_compat is_ipv4_mapped is_not is_not_null is_used_lock isdate isnull isolation join key kill language last last_day last_insert_id last_value lcase lead leading least leaves left len lenght level like limit lines ln load load_file local localtime localtimestamp locate lock log log10 log2 logfile logs low_priority lower lpad ltrim make_set makedate maketime master master_pos_wait match matched max md5 medium merge microsecond mid min minute mod mode module month monthname mutex name_const names national natural nchar next no no_write_to_binlog not now nullif nvarchar oct octet_length of old_password on only open optimize option optionally or ord order outer outfile output pad parse partial partition password patindex percent_rank percentile_cont percentile_disc period_add period_diff pi plugin position pow power pragma precision prepare preserve primary prior privileges procedure procedure_analyze processlist profile profiles public publishingservername purge quarter query quick quote quotename radians rand read references regexp relative relaylog release release_lock rename repair repeat replace replicate reset restore restrict return returns reverse revoke right rlike rollback rollup round row row_count rows rpad rtrim savepoint schema scroll sec_to_time second section select serializable server session session_user set sha sha1 sha2 share show sign sin size slave sleep smalldatetimefromparts snapshot some soname soundex sounds_like space sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_no_cache sql_small_result sql_variant_property sqlstate sqrt square start starting status std stddev stddev_pop stddev_samp stdev stdevp stop str str_to_date straight_join strcmp string stuff subdate substr substring subtime subtring_index sum switchoffset sysdate sysdatetime sysdatetimeoffset system_user sysutcdatetime table tables tablespace tan temporary terminated tertiary_weights then time time_format time_to_sec timediff timefromparts timestamp timestampadd timestampdiff timezone_hour timezone_minute to to_base64 to_days to_seconds todatetimeoffset trailing transaction translation trigger trigger_nestlevel triggers trim truncate try_cast try_convert try_parse ucase uncompress uncompressed_length unhex unicode uninstall union unique unix_timestamp unknown unlock update upgrade upped upper usage use user user_resources using utc_date utc_time utc_timestamp uuid uuid_short validate_password_strength value values var var_pop var_samp variables variance varp version view warnings week weekday weekofyear weight_string when whenever where with work write xml xor year yearweek zon",literal:"true false null",built_in:"array bigint binary bit blob boolean char character date dec decimal float int integer interval number numeric real serial smallint varchar varying int8 serial8 text"},c:[{cN:"string",b:"'",e:"'",c:[a.BE,{b:"''"}]},{cN:"string",b:'"',e:'"',c:[a.BE,{b:'""'}]},{cN:"string",b:"`",e:"`",c:[a.BE]},a.CNM,a.CBCM,b]},a.CBCM,b]}});hljs.registerLanguage("php",function(b){var e={cN:"variable",b:"(\\$|->)+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*"};var a={cN:"preprocessor",b:/<\?(php)?|\?>/};var c={cN:"string",c:[b.BE,a],v:[{b:'b"',e:'"'},{b:"b'",e:"'"},b.inherit(b.ASM,{i:null}),b.inherit(b.QSM,{i:null})]};var d={v:[b.BNM,b.CNM]};return{aliases:["php3","php4","php5","php6"],cI:true,k:"and include_once list abstract global private echo interface as static endswitch array null if endwhile or const for endforeach self var while isset public protected exit foreach throw elseif include __FILE__ empty require_once do xor return parent clone use __CLASS__ __LINE__ else break print eval new catch __METHOD__ case exception default die require __FUNCTION__ enddeclare final try switch continue endfor endif declare unset true false trait goto instanceof insteadof __DIR__ __NAMESPACE__ yield finally",c:[b.CLCM,b.HCM,{cN:"comment",b:"/\\*",e:"\\*/",c:[{cN:"phpdoc",b:"\\s@[A-Za-z]+"},a]},{cN:"comment",b:"__halt_compiler.+?;",eW:true,k:"__halt_compiler",l:b.UIR},{cN:"string",b:"<<<['\"]?\\w+['\"]?$",e:"^\\w+;",c:[b.BE]},a,e,{cN:"function",bK:"function",e:/[;{]/,eE:true,i:"\\$|\\[|%",c:[b.UTM,{cN:"params",b:"\\(",e:"\\)",c:["self",e,b.CBCM,c,d]}]},{cN:"class",bK:"class interface",e:"{",eE:true,i:/[:\(\$"]/,c:[{bK:"extends implements"},b.UTM]},{bK:"namespace",e:";",i:/[\.']/,c:[b.UTM]},{bK:"use",e:";",c:[b.UTM]},{b:"=>"},c,d]}});hljs.registerLanguage("makefile",function(a){var b={cN:"variable",b:/\$\(/,e:/\)/,c:[a.BE]};return{aliases:["mk","mak"],c:[a.HCM,{b:/^\w+\s*\W*=/,rB:true,r:0,starts:{cN:"constant",e:/\s*\W*=/,eE:true,starts:{e:/$/,r:0,c:[b]}}},{cN:"title",b:/^[\w]+:\s*$/},{cN:"phony",b:/^\.PHONY:/,e:/$/,k:".PHONY",l:/[\.\w]+/},{b:/^\t+/,e:/$/,r:0,c:[a.QSM,b]}]}});hljs.registerLanguage("bash",function(b){var a={cN:"variable",v:[{b:/\$[\w\d#@][\w\d_]*/},{b:/\$\{(.*?)\}/}]};var d={cN:"string",b:/"/,e:/"/,c:[b.BE,a,{cN:"variable",b:/\$\(/,e:/\)/,c:[b.BE]}]};var c={cN:"string",b:/'/,e:/'/};return{aliases:["sh","zsh"],l:/-?[a-z\.]+/,k:{keyword:"if then else elif fi for break continue while in do done exit return set declare case esac export exec",literal:"true false",built_in:"printf echo read cd pwd pushd popd dirs let eval unset typeset readonly getopts source shopt caller type hash bind help sudo",operator:"-ne -eq -lt -gt -f -d -e -s -l -a"},c:[{cN:"shebang",b:/^#![^\n]+sh\s*$/,r:10},{cN:"function",b:/\w[\w\d_]*\s*\(\s*\)\s*\{/,rB:true,c:[b.inherit(b.TM,{b:/\w[\w\d_]*/})],r:0},b.HCM,b.NM,d,c,a]}});hljs.registerLanguage("cpp",function(a){var b={keyword:"false int float while private char catch export virtual operator sizeof dynamic_cast|10 typedef const_cast|10 const struct for static_cast|10 union namespace unsigned long throw volatile static protected bool template mutable if public friend do return goto auto void enum else break new extern using true class asm case typeid short reinterpret_cast|10 default double register explicit signed typename try this switch continue wchar_t inline delete alignof char16_t char32_t constexpr decltype noexcept nullptr static_assert thread_local restrict _Bool complex _Complex _Imaginary",built_in:"std string cin cout cerr clog stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap array shared_ptr abort abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf"};return{aliases:["c","h","c++","h++"],k:b,i:"</",c:[a.CLCM,a.CBCM,a.QSM,{cN:"string",b:"'\\\\?.",e:"'",i:"."},{cN:"number",b:"\\b(\\d+(\\.\\d*)?|\\.\\d+)(u|U|l|L|ul|UL|f|F)"},a.CNM,{cN:"preprocessor",b:"#",e:"$",k:"if else elif endif define undef warning error line pragma",c:[{b:'include\\s*[<"]',e:'[>"]',k:"include",i:"\\n"},a.CLCM]},{cN:"stl_container",b:"\\b(deque|list|queue|stack|vector|map|set|bitset|multiset|multimap|unordered_map|unordered_set|unordered_multiset|unordered_multimap|array)\\s*<",e:">",k:b,c:["self"]},{b:a.IR+"::"}]}});hljs.registerLanguage("perl",function(c){var d="getpwent getservent quotemeta msgrcv scalar kill dbmclose undef lc ma syswrite tr send umask sysopen shmwrite vec qx utime local oct semctl localtime readpipe do return format read sprintf dbmopen pop getpgrp not getpwnam rewinddir qqfileno qw endprotoent wait sethostent bless s|0 opendir continue each sleep endgrent shutdown dump chomp connect getsockname die socketpair close flock exists index shmgetsub for endpwent redo lstat msgctl setpgrp abs exit select print ref gethostbyaddr unshift fcntl syscall goto getnetbyaddr join gmtime symlink semget splice x|0 getpeername recv log setsockopt cos last reverse gethostbyname getgrnam study formline endhostent times chop length gethostent getnetent pack getprotoent getservbyname rand mkdir pos chmod y|0 substr endnetent printf next open msgsnd readdir use unlink getsockopt getpriority rindex wantarray hex system getservbyport endservent int chr untie rmdir prototype tell listen fork shmread ucfirst setprotoent else sysseek link getgrgid shmctl waitpid unpack getnetbyname reset chdir grep split require caller lcfirst until warn while values shift telldir getpwuid my getprotobynumber delete and sort uc defined srand accept package seekdir getprotobyname semop our rename seek if q|0 chroot sysread setpwent no crypt getc chown sqrt write setnetent setpriority foreach tie sin msgget map stat getlogin unless elsif truncate exec keys glob tied closedirioctl socket readlink eval xor readline binmode setservent eof ord bind alarm pipe atan2 getgrent exp time push setgrent gt lt or ne m|0 break given say state when";var f={cN:"subst",b:"[$@]\\{",e:"\\}",k:d};var g={b:"->{",e:"}"};var a={cN:"variable",v:[{b:/\$\d/},{b:/[\$\%\@](\^\w\b|#\w+(\:\:\w+)*|{\w+}|\w+(\:\:\w*)*)/},{b:/[\$\%\@][^\s\w{]/,r:0}]};var e={cN:"comment",b:"^(__END__|__DATA__)",e:"\\n$",r:5};var h=[c.BE,f,a];var b=[a,c.HCM,e,{cN:"comment",b:"^\\=\\w",e:"\\=cut",eW:true},g,{cN:"string",c:h,v:[{b:"q[qwxr]?\\s*\\(",e:"\\)",r:5},{b:"q[qwxr]?\\s*\\[",e:"\\]",r:5},{b:"q[qwxr]?\\s*\\{",e:"\\}",r:5},{b:"q[qwxr]?\\s*\\|",e:"\\|",r:5},{b:"q[qwxr]?\\s*\\<",e:"\\>",r:5},{b:"qw\\s+q",e:"q",r:5},{b:"'",e:"'",c:[c.BE]},{b:'"',e:'"'},{b:"`",e:"`",c:[c.BE]},{b:"{\\w+}",c:[],r:0},{b:"-?\\w+\\s*\\=\\>",c:[],r:0}]},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{b:"(\\/\\/|"+c.RSR+"|\\b(split|return|print|reverse|grep)\\b)\\s*",k:"split return print reverse grep",r:0,c:[c.HCM,e,{cN:"regexp",b:"(s|tr|y)/(\\\\.|[^/])*/(\\\\.|[^/])*/[a-z]*",r:10},{cN:"regexp",b:"(m|qr)?/",e:"/[a-z]*",c:[c.BE],r:0}]},{cN:"sub",bK:"sub",e:"(\\s*\\(.*?\\))?[;{]",r:5},{cN:"operator",b:"-\\w\\b",r:0}];f.c=b;g.c=b;return{aliases:["pl"],k:d,c:b}});hljs.registerLanguage("ini",function(a){return{cI:true,i:/\S/,c:[{cN:"comment",b:";",e:"$"},{cN:"title",b:"^\\[",e:"\\]"},{cN:"setting",b:"^[a-z0-9\\[\\]_-]+[ \\t]*=[ \\t]*",e:"$",c:[{cN:"value",eW:true,k:"on off true false yes no",c:[a.QSM,a.NM],r:0}]}]}});hljs.registerLanguage("apache",function(a){var b={cN:"number",b:"[\\$%]\\d+"};return{aliases:["apacheconf"],cI:true,c:[a.HCM,{cN:"tag",b:"</?",e:">"},{cN:"keyword",b:/\w+/,r:0,k:{common:"order deny allow setenv rewriterule rewriteengine rewritecond documentroot sethandler errordocument loadmodule options header listen serverroot servername"},starts:{e:/$/,r:0,k:{literal:"on off all"},c:[{cN:"sqbracket",b:"\\s\\[",e:"\\]$"},{cN:"cbracket",b:"[\\$%]\\{",e:"\\}",c:["self",b]},b,a.QSM]}}],i:/\S/}});hljs.registerLanguage("java",function(c){var b=c.UIR+"(<"+c.UIR+">)?";var a="false synchronized int abstract float private char boolean static null if const for true while long throw strictfp finally protected import native final return void enum else break transient new catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private";return{aliases:["jsp"],k:a,i:/<\//,c:[{cN:"javadoc",b:"/\\*\\*",e:"\\*/",r:0,c:[{cN:"javadoctag",b:"(^|\\s)@[A-Za-z]+"}]},c.CLCM,c.CBCM,c.ASM,c.QSM,{cN:"class",bK:"class interface",e:/[{;=]/,eE:true,k:"class interface",i:/[:"\[\]]/,c:[{bK:"extends implements"},c.UTM]},{bK:"new",e:/\s/,r:0},{cN:"function",b:"("+b+"\\s+)+"+c.UIR+"\\s*\\(",rB:true,e:/[{;=]/,eE:true,k:a,c:[{b:c.UIR+"\\s*\\(",rB:true,c:[c.UTM]},{cN:"params",b:/\(/,e:/\)/,k:a,c:[c.ASM,c.QSM,c.CNM,c.CBCM]},c.CLCM,c.CBCM]},c.CNM,{cN:"annotation",b:"@[A-Za-z]+"}]}});hljs.registerLanguage("xml",function(a){var c="[A-Za-z0-9\\._:-]+";var d={b:/<\?(php)?(?!\w)/,e:/\?>/,sL:"php",subLanguageMode:"continuous"};var b={eW:true,i:/</,r:0,c:[d,{cN:"attribute",b:c,r:0},{b:"=",r:0,c:[{cN:"value",v:[{b:/"/,e:/"/},{b:/'/,e:/'/},{b:/[^\s\/>]+/}]}]}]};return{aliases:["html","xhtml","rss","atom","xsl","plist"],cI:true,c:[{cN:"doctype",b:"<!DOCTYPE",e:">",r:10,c:[{b:"\\[",e:"\\]"}]},{cN:"comment",b:"<!--",e:"-->",r:10},{cN:"cdata",b:"<\\!\\[CDATA\\[",e:"\\]\\]>",r:10},{cN:"tag",b:"<style(?=\\s|>|$)",e:">",k:{title:"style"},c:[b],starts:{e:"</style>",rE:true,sL:"css"}},{cN:"tag",b:"<script(?=\\s|>|$)",e:">",k:{title:"script"},c:[b],starts:{e:"<\/script>",rE:true,sL:"javascript"}},{b:"<%",e:"%>",sL:"vbscript"},d,{cN:"pi",b:/<\?\w+/,e:/\?>/,r:10},{cN:"tag",b:"</?",e:"/?>",c:[{cN:"title",b:/[^ \/><\n\t]+/,r:0},b]}]}});hljs.registerLanguage("markdown",function(a){return{aliases:["md","mkdown","mkd"],c:[{cN:"header",v:[{b:"^#{1,6}",e:"$"},{b:"^.+?\\n[=-]{2,}$"}]},{b:"<",e:">",sL:"xml",r:0},{cN:"bullet",b:"^([*+-]|(\\d+\\.))\\s+"},{cN:"strong",b:"[*_]{2}.+?[*_]{2}"},{cN:"emphasis",v:[{b:"\\*.+?\\*"},{b:"_.+?_",r:0}]},{cN:"blockquote",b:"^>\\s+",e:"$"},{cN:"code",v:[{b:"`.+?`"},{b:"^( {4}|\t)",e:"$",r:0}]},{cN:"horizontal_rule",b:"^[-\\*]{3,}",e:"$"},{b:"\\[.+?\\][\\(\\[].*?[\\)\\]]",rB:true,c:[{cN:"link_label",b:"\\[",e:"\\]",eB:true,rE:true,r:0},{cN:"link_url",b:"\\]\\(",e:"\\)",eB:true,eE:true},{cN:"link_reference",b:"\\]\\[",e:"\\]",eB:true,eE:true}],r:10},{b:"^\\[.+\\]:",rB:true,c:[{cN:"link_reference",b:"\\[",e:"\\]:",eB:true,eE:true,starts:{cN:"link_url",e:"$"}}]}]}});hljs.registerLanguage("cs",function(c){var b="abstract as base bool break byte case catch char checked const continue decimal default delegate do double else enum event explicit extern false finally fixed float for foreach goto if implicit in int interface internal is lock long new null object operator out override params private protected public readonly ref return sbyte sealed short sizeof stackalloc static string struct switch this throw true try typeof uint ulong unchecked unsafe ushort using virtual volatile void while async await protected public private internal ascending descending from get group into join let orderby partial select set value var where yield";var a=c.IR+"(<"+c.IR+">)?";return{aliases:["csharp"],k:b,i:/::/,c:[{cN:"comment",b:"///",e:"$",rB:true,c:[{cN:"xmlDocTag",v:[{b:"///",r:0},{b:"<!--|-->"},{b:"</?",e:">"}]}]},c.CLCM,c.CBCM,{cN:"preprocessor",b:"#",e:"$",k:"if else elif endif define undef warning error line region endregion pragma checksum"},{cN:"string",b:'@"',e:'"',c:[{b:'""'}]},c.ASM,c.QSM,c.CNM,{bK:"class namespace interface",e:/[{;=]/,i:/[^\s:]/,c:[c.TM,c.CLCM,c.CBCM]},{bK:"new",e:/\s/,r:0},{cN:"function",b:"("+a+"\\s+)+"+c.IR+"\\s*\\(",rB:true,e:/[{;=]/,eE:true,k:b,c:[{b:c.IR+"\\s*\\(",rB:true,c:[c.TM]},{cN:"params",b:/\(/,e:/\)/,k:b,c:[c.ASM,c.QSM,c.CNM,c.CBCM]},c.CLCM,c.CBCM]}]}});hljs.registerLanguage("ruby",function(f){var j="[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?";var i="and false then defined module in return redo if BEGIN retry end for true self when next until do begin unless END rescue nil else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor";var b={cN:"yardoctag",b:"@[A-Za-z]+"};var c={cN:"value",b:"#<",e:">"};var k={cN:"comment",v:[{b:"#",e:"$",c:[b]},{b:"^\\=begin",e:"^\\=end",c:[b],r:10},{b:"^__END__",e:"\\n$"}]};var d={cN:"subst",b:"#\\{",e:"}",k:i};var e={cN:"string",c:[f.BE,d],v:[{b:/'/,e:/'/},{b:/"/,e:/"/},{b:"%[qw]?\\(",e:"\\)"},{b:"%[qw]?\\[",e:"\\]"},{b:"%[qw]?{",e:"}"},{b:"%[qw]?<",e:">"},{b:"%[qw]?/",e:"/"},{b:"%[qw]?%",e:"%"},{b:"%[qw]?-",e:"-"},{b:"%[qw]?\\|",e:"\\|"},{b:/\B\?(\\\d{1,3}|\\x[A-Fa-f0-9]{1,2}|\\u[A-Fa-f0-9]{4}|\\?\S)\b/}]};var a={cN:"params",b:"\\(",e:"\\)",k:i};var h=[e,c,k,{cN:"class",bK:"class module",e:"$|;",i:/=/,c:[f.inherit(f.TM,{b:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{cN:"inheritance",b:"<\\s*",c:[{cN:"parent",b:"("+f.IR+"::)?"+f.IR}]},k]},{cN:"function",bK:"def",e:" |$|;",r:0,c:[f.inherit(f.TM,{b:j}),a,k]},{cN:"constant",b:"(::)?(\\b[A-Z]\\w*(::)?)+",r:0},{cN:"symbol",b:f.UIR+"(\\!|\\?)?:",r:0},{cN:"symbol",b:":",c:[e,{b:j}],r:0},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{cN:"variable",b:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{b:"("+f.RSR+")\\s*",c:[c,k,{cN:"regexp",c:[f.BE,d],i:/\n/,v:[{b:"/",e:"/[a-z]*"},{b:"%r{",e:"}[a-z]*"},{b:"%r\\(",e:"\\)[a-z]*"},{b:"%r!",e:"![a-z]*"},{b:"%r\\[",e:"\\][a-z]*"}]}],r:0}];d.c=h;a.c=h;var g=[{b:/^\s*=>/,cN:"status",starts:{e:"$",c:h}},{cN:"prompt",b:/^\S[^=>\n]*>+/,starts:{e:"$",c:h}}];return{aliases:["rb","gemspec","podspec","thor","irb"],k:i,c:[k].concat(g).concat(h)}});hljs.registerLanguage("diff",function(a){return{aliases:["patch"],c:[{cN:"chunk",r:10,v:[{b:/^\@\@ +\-\d+,\d+ +\+\d+,\d+ +\@\@$/},{b:/^\*\*\* +\d+,\d+ +\*\*\*\*$/},{b:/^\-\-\- +\d+,\d+ +\-\-\-\-$/}]},{cN:"header",v:[{b:/Index: /,e:/$/},{b:/=====/,e:/=====$/},{b:/^\-\-\-/,e:/$/},{b:/^\*{3} /,e:/$/},{b:/^\+\+\+/,e:/$/},{b:/\*{5}/,e:/\*{5}$/}]},{cN:"addition",b:"^\\+",e:"$"},{cN:"deletion",b:"^\\-",e:"$"},{cN:"change",b:"^\\!",e:"$"}]}});hljs.registerLanguage("objectivec",function(a){var d={keyword:"int float while char export sizeof typedef const struct for union unsigned long volatile static bool mutable if do return goto void enum else break extern asm case short default double register explicit signed typename this switch continue wchar_t inline readonly assign readwrite self @synchronized id typeof nonatomic super unichar IBOutlet IBAction strong weak copy in out inout bycopy byref oneway __strong __weak __block __autoreleasing @private @protected @public @try @property @end @throw @catch @finally @autoreleasepool @synthesize @dynamic @selector @optional @required",literal:"false true FALSE TRUE nil YES NO NULL",built_in:"NSString NSData NSDictionary CGRect CGPoint UIButton UILabel UITextView UIWebView MKMapView NSView NSViewController NSWindow NSWindowController NSSet NSUUID NSIndexSet UISegmentedControl NSObject UITableViewDelegate UITableViewDataSource NSThread UIActivityIndicator UITabbar UIToolBar UIBarButtonItem UIImageView NSAutoreleasePool UITableView BOOL NSInteger CGFloat NSException NSLog NSMutableString NSMutableArray NSMutableDictionary NSURL NSIndexPath CGSize UITableViewCell UIView UIViewController UINavigationBar UINavigationController UITabBarController UIPopoverController UIPopoverControllerDelegate UIImage NSNumber UISearchBar NSFetchedResultsController NSFetchedResultsChangeType UIScrollView UIScrollViewDelegate UIEdgeInsets UIColor UIFont UIApplication NSNotFound NSNotificationCenter NSNotification UILocalNotification NSBundle NSFileManager NSTimeInterval NSDate NSCalendar NSUserDefaults UIWindow NSRange NSArray NSError NSURLRequest NSURLConnection NSURLSession NSURLSessionDataTask NSURLSessionDownloadTask NSURLSessionUploadTask NSURLResponseUIInterfaceOrientation MPMoviePlayerController dispatch_once_t dispatch_queue_t dispatch_sync dispatch_async dispatch_once"};var c=/[a-zA-Z@][a-zA-Z0-9_]*/;var b="@interface @class @protocol @implementation";return{aliases:["m","mm","objc","obj-c"],k:d,l:c,i:"</",c:[a.CLCM,a.CBCM,a.CNM,a.QSM,{cN:"string",v:[{b:'@"',e:'"',i:"\\n",c:[a.BE]},{b:"'",e:"[^\\\\]'",i:"[^\\\\][^']"}]},{cN:"preprocessor",b:"#",e:"$",c:[{cN:"title",v:[{b:'"',e:'"'},{b:"<",e:">"}]}]},{cN:"class",b:"("+b.split(" ").join("|")+")\\b",e:"({|$)",eE:true,k:b,l:c,c:[a.UTM]},{cN:"variable",b:"\\."+a.UIR,r:0}]}});hljs.registerLanguage("css",function(a){var b="[a-zA-Z-][a-zA-Z0-9_-]*";var c={cN:"function",b:b+"\\(",rB:true,eE:true,e:"\\("};return{cI:true,i:"[=/|']",c:[a.CBCM,{cN:"id",b:"\\#[A-Za-z0-9_-]+"},{cN:"class",b:"\\.[A-Za-z0-9_-]+",r:0},{cN:"attr_selector",b:"\\[",e:"\\]",i:"$"},{cN:"pseudo",b:":(:)?[a-zA-Z0-9\\_\\-\\+\\(\\)\\\"\\']+"},{cN:"at_rule",b:"@(font-face|page)",l:"[a-z-]+",k:"font-face page"},{cN:"at_rule",b:"@",e:"[{;]",c:[{cN:"keyword",b:/\S+/},{b:/\s/,eW:true,eE:true,r:0,c:[c,a.ASM,a.QSM,a.CSSNM]}]},{cN:"tag",b:b,r:0},{cN:"rules",b:"{",e:"}",i:"[^\\s]",r:0,c:[a.CBCM,{cN:"rule",b:"[^\\s]",rB:true,e:";",eW:true,c:[{cN:"attribute",b:"[A-Z\\_\\.\\-]+",e:":",eE:true,i:"[^\\s]",starts:{cN:"value",eW:true,eE:true,c:[c,a.CSSNM,a.QSM,a.ASM,a.CBCM,{cN:"hexcolor",b:"#[0-9A-Fa-f]+"},{cN:"important",b:"!important"}]}}]}]}]}});hljs.registerLanguage("python",function(a){var f={cN:"prompt",b:/^(>>>|\.\.\.) /};var b={cN:"string",c:[a.BE],v:[{b:/(u|b)?r?'''/,e:/'''/,c:[f],r:10},{b:/(u|b)?r?"""/,e:/"""/,c:[f],r:10},{b:/(u|r|ur)'/,e:/'/,r:10},{b:/(u|r|ur)"/,e:/"/,r:10},{b:/(b|br)'/,e:/'/},{b:/(b|br)"/,e:/"/},a.ASM,a.QSM]};var d={cN:"number",r:0,v:[{b:a.BNR+"[lLjJ]?"},{b:"\\b(0o[0-7]+)[lLjJ]?"},{b:a.CNR+"[lLjJ]?"}]};var e={cN:"params",b:/\(/,e:/\)/,c:["self",f,d,b]};var c={e:/:/,i:/[${=;\n]/,c:[a.UTM,e]};return{aliases:["py","gyp"],k:{keyword:"and elif is global as in if from raise for except finally print import pass return exec else break not with class assert yield try while continue del or def lambda nonlocal|10 None True False",built_in:"Ellipsis NotImplemented"},i:/(<\/|->|\?)/,c:[f,d,b,a.HCM,a.inherit(c,{cN:"function",bK:"def",r:10}),a.inherit(c,{cN:"class",bK:"class"}),{cN:"decorator",b:/@/,e:/$/},{b:/\b(print|exec)\(/}]}});
+4
View File
@@ -0,0 +1,4 @@
/*! jQuery v3.2.1 | (c) JS Foundation and other contributors | jquery.org/license */
!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.2.1",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c<b?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:h,sort:c.sort,splice:c.splice},r.extend=r.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||r.isFunction(g)||(g={}),h===i&&(g=this,h--);h<i;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(r.isPlainObject(d)||(e=Array.isArray(d)))?(e?(e=!1,f=c&&Array.isArray(c)?c:[]):f=c&&r.isPlainObject(c)?c:{},g[b]=r.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},r.extend({expando:"jQuery"+(q+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===r.type(a)},isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){var b=r.type(a);return("number"===b||"string"===b)&&!isNaN(a-parseFloat(a))},isPlainObject:function(a){var b,c;return!(!a||"[object Object]"!==k.call(a))&&(!(b=e(a))||(c=l.call(b,"constructor")&&b.constructor,"function"==typeof c&&m.call(c)===n))},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?j[k.call(a)]||"object":typeof a},globalEval:function(a){p(a)},camelCase:function(a){return a.replace(t,"ms-").replace(u,v)},each:function(a,b){var c,d=0;if(w(a)){for(c=a.length;d<c;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(s,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(w(Object(a))?r.merge(c,"string"==typeof a?[a]:a):h.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:i.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;d<c;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;f<g;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,f=0,h=[];if(w(a))for(d=a.length;f<d;f++)e=b(a[f],f,c),null!=e&&h.push(e);else for(f in a)e=b(a[f],f,c),null!=e&&h.push(e);return g.apply([],h)},guid:1,proxy:function(a,b){var c,d,e;if("string"==typeof b&&(c=a[b],b=a,a=c),r.isFunction(a))return d=f.call(arguments,2),e=function(){return a.apply(b||this,d.concat(f.call(arguments)))},e.guid=a.guid=a.guid||r.guid++,e},now:Date.now,support:o}),"function"==typeof Symbol&&(r.fn[Symbol.iterator]=c[Symbol.iterator]),r.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(a,b){j["[object "+b+"]"]=b.toLowerCase()});function w(a){var b=!!a&&"length"in a&&a.length,c=r.type(a);return"function"!==c&&!r.isWindow(a)&&("array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c<d;c++)if(a[c]===b)return c;return-1},J="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",K="[\\x20\\t\\r\\n\\f]",L="(?:\\\\.|[\\w-]|[^\0-\\xa0])+",M="\\["+K+"*("+L+")(?:"+K+"*([*^$|!~]?=)"+K+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+L+"))|)"+K+"*\\]",N=":("+L+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+M+")*)|.*)\\)|)",O=new RegExp(K+"+","g"),P=new RegExp("^"+K+"+|((?:^|[^\\\\])(?:\\\\.)*)"+K+"+$","g"),Q=new RegExp("^"+K+"*,"+K+"*"),R=new RegExp("^"+K+"*([>+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="<a id='"+u+"'></a><select id='"+u+"-\r\\' msallowcapture=''><option selected=''></option></select>",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="<a href='' disabled='disabled'></a><select disabled='disabled'><option/></select>";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c<b;c+=2)a.push(c);return a}),odd:pa(function(a,b){for(var c=1;c<b;c+=2)a.push(c);return a}),lt:pa(function(a,b,c){for(var d=c<0?c+b:c;--d>=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d<b;)a.push(d);return a})}},d.pseudos.nth=d.pseudos.eq;for(b in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})d.pseudos[b]=ma(b);for(b in{submit:!0,reset:!0})d.pseudos[b]=na(b);function ra(){}ra.prototype=d.filters=d.pseudos,d.setFilters=new ra,g=ga.tokenize=function(a,b){var c,e,f,g,h,i,j,k=z[a+" "];if(k)return b?0:k.slice(0);h=a,i=[],j=d.preFilter;while(h){c&&!(e=Q.exec(h))||(e&&(h=h.slice(e[0].length)||h),i.push(f=[])),c=!1,(e=R.exec(h))&&(c=e.shift(),f.push({value:c,type:e[0].replace(P," ")}),h=h.slice(c.length));for(g in d.filter)!(e=V[g].exec(h))||j[g]&&!(e=j[g](e))||(c=e.shift(),f.push({value:c,type:g,matches:e}),h=h.slice(c.length));if(!c)break}return b?h.length:h?ga.error(a):z(a,i).slice(0)};function sa(a){for(var b=0,c=a.length,d="";b<c;b++)d+=a[b].value;return d}function ta(a,b,c){var d=b.dir,e=b.next,f=e||d,g=c&&"parentNode"===f,h=x++;return b.first?function(b,c,e){while(b=b[d])if(1===b.nodeType||g)return a(b,c,e);return!1}:function(b,c,i){var j,k,l,m=[w,h];if(i){while(b=b[d])if((1===b.nodeType||g)&&a(b,c,i))return!0}else while(b=b[d])if(1===b.nodeType||g)if(l=b[u]||(b[u]={}),k=l[b.uniqueID]||(l[b.uniqueID]={}),e&&e===b.nodeName.toLowerCase())b=b[d]||b;else{if((j=k[f])&&j[0]===w&&j[1]===h)return m[2]=j[2];if(k[f]=m,m[2]=a(b,c,i))return!0}return!1}}function ua(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d<e;d++)ga(a,b[d],c);return c}function wa(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;h<i;h++)(f=a[h])&&(c&&!c(f,d,e)||(g.push(f),j&&b.push(h)));return g}function xa(a,b,c,d,e,f){return d&&!d[u]&&(d=xa(d)),e&&!e[u]&&(e=xa(e,f)),ia(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||va(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:wa(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=wa(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?I(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i<f;i++)if(c=d.relative[a[i].type])m=[ta(ua(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;e<f;e++)if(d.relative[a[e].type])break;return xa(i>1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i<e&&ya(a.slice(i,e)),e<f&&ya(a=a.slice(e)),e<f&&sa(a))}m.push(c)}return ua(m)}function za(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="<a href='#'></a>","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="<input/>",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext;function B(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()}var C=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,D=/^.[^:#\[\.,]*$/;function E(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):D.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b<d;b++)if(r.contains(e[b],this))return!0}));for(c=this.pushStack([]),b=0;b<d;b++)r.find(a,e[b],c);return d>1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(E(this,a||[],!1))},not:function(a){return this.pushStack(E(this,a||[],!0))},is:function(a){return!!E(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var F,G=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,H=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||F,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:G.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),C.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};H.prototype=r.fn,F=r(d);var I=/^(?:parents|prev(?:Until|All))/,J={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a<c;a++)if(r.contains(this,b[a]))return!0})},closest:function(a,b){var c,d=0,e=this.length,f=[],g="string"!=typeof a&&r(a);if(!A.test(a))for(;d<e;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function K(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return K(a,"nextSibling")},prev:function(a){return K(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return B(a,"iframe")?a.contentDocument:(B(a,"template")&&(a=a.content||a),r.merge([],a.childNodes))}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(J[a]||r.uniqueSort(e),I.test(a)&&e.reverse()),this.pushStack(e)}});var L=/[^\x20\t\r\n\f]+/g;function M(a){var b={};return r.each(a.match(L)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?M(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=e||a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h<f.length)f[h].apply(c[0],c[1])===!1&&a.stopOnFalse&&(h=f.length,c=!1)}a.memory||(c=!1),b=!1,e&&(f=c?[]:"")},j={add:function(){return f&&(c&&!b&&(h=f.length-1,g.push(c)),function d(b){r.each(b,function(b,c){r.isFunction(c)?a.unique&&j.has(c)||f.push(c):c&&c.length&&"string"!==r.type(c)&&d(c)})}(arguments),c&&!b&&i()),this},remove:function(){return r.each(arguments,function(a,b){var c;while((c=r.inArray(b,f,c))>-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function N(a){return a}function O(a){throw a}function P(a,b,c,d){var e;try{a&&r.isFunction(e=a.promise)?e.call(a).done(b).fail(c):a&&r.isFunction(e=a.then)?e.call(a,b,c):b.apply(void 0,[a].slice(d))}catch(a){c.apply(void 0,[a])}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b<f)){if(a=d.apply(h,i),a===c.promise())throw new TypeError("Thenable self-resolution");j=a&&("object"==typeof a||"function"==typeof a)&&a.then,r.isFunction(j)?e?j.call(a,g(f,c,N,e),g(f,c,O,e)):(f++,j.call(a,g(f,c,N,e),g(f,c,O,e),g(f,c,N,c.notifyWith))):(d!==N&&(h=void 0,i=[a]),(e||c.resolveWith)(h,i))}},k=e?j:function(){try{j()}catch(a){r.Deferred.exceptionHook&&r.Deferred.exceptionHook(a,k.stackTrace),b+1>=f&&(d!==O&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:N,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:N)),c[2][3].add(g(0,a,r.isFunction(d)?d:O))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(P(a,g.done(h(c)).resolve,g.reject,!b),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)P(e[c],h(c),g.reject);return g.promise()}});var Q=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&Q.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var R=r.Deferred();r.fn.ready=function(a){return R.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||R.resolveWith(d,[r]))}}),r.ready.then=R.then;function S(){d.removeEventListener("DOMContentLoaded",S),
a.removeEventListener("load",S),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",S),a.addEventListener("load",S));var T=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)T(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h<i;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},U=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function V(){this.expando=r.expando+V.uid++}V.uid=1,V.prototype={cache:function(a){var b=a[this.expando];return b||(b={},U(a)&&(a.nodeType?a[this.expando]=b:Object.defineProperty(a,this.expando,{value:b,configurable:!0}))),b},set:function(a,b,c){var d,e=this.cache(a);if("string"==typeof b)e[r.camelCase(b)]=c;else for(d in b)e[r.camelCase(d)]=b[d];return e},get:function(a,b){return void 0===b?this.cache(a):a[this.expando]&&a[this.expando][r.camelCase(b)]},access:function(a,b,c){return void 0===b||b&&"string"==typeof b&&void 0===c?this.get(a,b):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d=a[this.expando];if(void 0!==d){if(void 0!==b){Array.isArray(b)?b=b.map(r.camelCase):(b=r.camelCase(b),b=b in d?[b]:b.match(L)||[]),c=b.length;while(c--)delete d[b[c]]}(void 0===b||r.isEmptyObject(d))&&(a.nodeType?a[this.expando]=void 0:delete a[this.expando])}},hasData:function(a){var b=a[this.expando];return void 0!==b&&!r.isEmptyObject(b)}};var W=new V,X=new V,Y=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,Z=/[A-Z]/g;function $(a){return"true"===a||"false"!==a&&("null"===a?null:a===+a+""?+a:Y.test(a)?JSON.parse(a):a)}function _(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(Z,"-$&").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c=$(c)}catch(e){}X.set(a,b,c)}else c=void 0;return c}r.extend({hasData:function(a){return X.hasData(a)||W.hasData(a)},data:function(a,b,c){return X.access(a,b,c)},removeData:function(a,b){X.remove(a,b)},_data:function(a,b,c){return W.access(a,b,c)},_removeData:function(a,b){W.remove(a,b)}}),r.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=X.get(f),1===f.nodeType&&!W.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=r.camelCase(d.slice(5)),_(f,d,e[d])));W.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){X.set(this,a)}):T(this,function(b){var c;if(f&&void 0===b){if(c=X.get(f,a),void 0!==c)return c;if(c=_(f,a),void 0!==c)return c}else this.each(function(){X.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){X.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=W.get(a,b),c&&(!d||Array.isArray(c)?d=W.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return W.get(a,c)||W.access(a,c,{empty:r.Callbacks("once memory").add(function(){W.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length<c?r.queue(this[0],a):void 0===b?this:this.each(function(){var c=r.queue(this,a,b);r._queueHooks(this,a),"fx"===a&&"inprogress"!==c[0]&&r.dequeue(this,a)})},dequeue:function(a){return this.each(function(){r.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,b){var c,d=1,e=r.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};"string"!=typeof a&&(b=a,a=void 0),a=a||"fx";while(g--)c=W.get(f[g],a+"queueHooks"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}});var aa=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,ba=new RegExp("^(?:([+-])=|)("+aa+")([a-z%]*)$","i"),ca=["Top","Right","Bottom","Left"],da=function(a,b){return a=b||a,"none"===a.style.display||""===a.style.display&&r.contains(a.ownerDocument,a)&&"none"===r.css(a,"display")},ea=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e};function fa(a,b,c,d){var e,f=1,g=20,h=d?function(){return d.cur()}:function(){return r.css(a,b,"")},i=h(),j=c&&c[3]||(r.cssNumber[b]?"":"px"),k=(r.cssNumber[b]||"px"!==j&&+i)&&ba.exec(r.css(a,b));if(k&&k[3]!==j){j=j||k[3],c=c||[],k=+i||1;do f=f||".5",k/=f,r.style(a,b,k+j);while(f!==(f=h()/i)&&1!==f&&--g)}return c&&(k=+k||+i||0,e=c[1]?k+(c[1]+1)*c[2]:+c[2],d&&(d.unit=j,d.start=k,d.end=e)),e}var ga={};function ha(a){var b,c=a.ownerDocument,d=a.nodeName,e=ga[d];return e?e:(b=c.body.appendChild(c.createElement(d)),e=r.css(b,"display"),b.parentNode.removeChild(b),"none"===e&&(e="block"),ga[d]=e,e)}function ia(a,b){for(var c,d,e=[],f=0,g=a.length;f<g;f++)d=a[f],d.style&&(c=d.style.display,b?("none"===c&&(e[f]=W.get(d,"display")||null,e[f]||(d.style.display="")),""===d.style.display&&da(d)&&(e[f]=ha(d))):"none"!==c&&(e[f]="none",W.set(d,"display",c)));for(f=0;f<g;f++)null!=e[f]&&(a[f].style.display=e[f]);return a}r.fn.extend({show:function(){return ia(this,!0)},hide:function(){return ia(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){da(this)?r(this).show():r(this).hide()})}});var ja=/^(?:checkbox|radio)$/i,ka=/<([a-z][^\/\0>\x20\t\r\n\f]+)/i,la=/^$|\/(?:java|ecma)script/i,ma={option:[1,"<select multiple='multiple'>","</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};ma.optgroup=ma.option,ma.tbody=ma.tfoot=ma.colgroup=ma.caption=ma.thead,ma.th=ma.td;function na(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&B(a,b)?r.merge([a],c):c}function oa(a,b){for(var c=0,d=a.length;c<d;c++)W.set(a[c],"globalEval",!b||W.get(b[c],"globalEval"))}var pa=/<|&#?\w+;/;function qa(a,b,c,d,e){for(var f,g,h,i,j,k,l=b.createDocumentFragment(),m=[],n=0,o=a.length;n<o;n++)if(f=a[n],f||0===f)if("object"===r.type(f))r.merge(m,f.nodeType?[f]:f);else if(pa.test(f)){g=g||l.appendChild(b.createElement("div")),h=(ka.exec(f)||["",""])[1].toLowerCase(),i=ma[h]||ma._default,g.innerHTML=i[1]+r.htmlPrefilter(f)+i[2],k=i[0];while(k--)g=g.lastChild;r.merge(m,g.childNodes),g=l.firstChild,g.textContent=""}else m.push(b.createTextNode(f));l.textContent="",n=0;while(f=m[n++])if(d&&r.inArray(f,d)>-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=na(l.appendChild(f),"script"),j&&oa(g),c){k=0;while(f=g[k++])la.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="<textarea>x</textarea>",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var ra=d.documentElement,sa=/^key/,ta=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ua=/^([^.]*)(?:\.(.+)|)/;function va(){return!0}function wa(){return!1}function xa(){try{return d.activeElement}catch(a){}}function ya(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)ya(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=wa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(ra,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(L)||[""],j=b.length;while(j--)h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.hasData(a)&&W.get(a);if(q&&(i=q.events)){b=(b||"").match(L)||[""],j=b.length;while(j--)if(h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&W.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(W.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c<arguments.length;c++)i[c]=arguments[c];if(b.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,b)!==!1){h=r.event.handlers.call(this,b,j),c=0;while((f=h[c++])&&!b.isPropagationStopped()){b.currentTarget=f.elem,d=0;while((g=f.handlers[d++])&&!b.isImmediatePropagationStopped())b.rnamespace&&!b.rnamespace.test(g.namespace)||(b.handleObj=g,b.data=g.data,e=((r.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(b.result=e)===!1&&(b.preventDefault(),b.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,b),b.result}},handlers:function(a,b){var c,d,e,f,g,h=[],i=b.delegateCount,j=a.target;if(i&&j.nodeType&&!("click"===a.type&&a.button>=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c<i;c++)d=b[c],e=d.selector+" ",void 0===g[e]&&(g[e]=d.needsContext?r(e,this).index(j)>-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i<b.length&&h.push({elem:j,handlers:b.slice(i)}),h},addProp:function(a,b){Object.defineProperty(r.Event.prototype,a,{enumerable:!0,configurable:!0,get:r.isFunction(b)?function(){if(this.originalEvent)return b(this.originalEvent)}:function(){if(this.originalEvent)return this.originalEvent[a]},set:function(b){Object.defineProperty(this,a,{enumerable:!0,configurable:!0,writable:!0,value:b})}})},fix:function(a){return a[r.expando]?a:new r.Event(a)},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==xa()&&this.focus)return this.focus(),!1},delegateType:"focusin"},blur:{trigger:function(){if(this===xa()&&this.blur)return this.blur(),!1},delegateType:"focusout"},click:{trigger:function(){if("checkbox"===this.type&&this.click&&B(this,"input"))return this.click(),!1},_default:function(a){return B(a.target,"a")}},beforeunload:{postDispatch:function(a){void 0!==a.result&&a.originalEvent&&(a.originalEvent.returnValue=a.result)}}}},r.removeEvent=function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c)},r.Event=function(a,b){return this instanceof r.Event?(a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||void 0===a.defaultPrevented&&a.returnValue===!1?va:wa,this.target=a.target&&3===a.target.nodeType?a.target.parentNode:a.target,this.currentTarget=a.currentTarget,this.relatedTarget=a.relatedTarget):this.type=a,b&&r.extend(this,b),this.timeStamp=a&&a.timeStamp||r.now(),void(this[r.expando]=!0)):new r.Event(a,b)},r.Event.prototype={constructor:r.Event,isDefaultPrevented:wa,isPropagationStopped:wa,isImmediatePropagationStopped:wa,isSimulated:!1,preventDefault:function(){var a=this.originalEvent;this.isDefaultPrevented=va,a&&!this.isSimulated&&a.preventDefault()},stopPropagation:function(){var a=this.originalEvent;this.isPropagationStopped=va,a&&!this.isSimulated&&a.stopPropagation()},stopImmediatePropagation:function(){var a=this.originalEvent;this.isImmediatePropagationStopped=va,a&&!this.isSimulated&&a.stopImmediatePropagation(),this.stopPropagation()}},r.each({altKey:!0,bubbles:!0,cancelable:!0,changedTouches:!0,ctrlKey:!0,detail:!0,eventPhase:!0,metaKey:!0,pageX:!0,pageY:!0,shiftKey:!0,view:!0,"char":!0,charCode:!0,key:!0,keyCode:!0,button:!0,buttons:!0,clientX:!0,clientY:!0,offsetX:!0,offsetY:!0,pointerId:!0,pointerType:!0,screenX:!0,screenY:!0,targetTouches:!0,toElement:!0,touches:!0,which:function(a){var b=a.button;return null==a.which&&sa.test(a.type)?null!=a.charCode?a.charCode:a.keyCode:!a.which&&void 0!==b&&ta.test(a.type)?1&b?1:2&b?3:4&b?2:0:a.which}},r.event.addProp),r.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(a,b){r.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj;return e&&(e===d||r.contains(d,e))||(a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b),c}}}),r.fn.extend({on:function(a,b,c,d){return ya(this,a,b,c,d)},one:function(a,b,c,d){return ya(this,a,b,c,d,1)},off:function(a,b,c){var d,e;if(a&&a.preventDefault&&a.handleObj)return d=a.handleObj,r(a.delegateTarget).off(d.namespace?d.origType+"."+d.namespace:d.origType,d.selector,d.handler),this;if("object"==typeof a){for(e in a)this.off(e,b,a[e]);return this}return b!==!1&&"function"!=typeof b||(c=b,b=void 0),c===!1&&(c=wa),this.each(function(){r.event.remove(this,a,c,b)})}});var za=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi,Aa=/<script|<style|<link/i,Ba=/checked\s*(?:[^=]|=\s*.checked.)/i,Ca=/^true\/(.*)/,Da=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g;function Ea(a,b){return B(a,"table")&&B(11!==b.nodeType?b:b.firstChild,"tr")?r(">tbody",a)[0]||a:a}function Fa(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Ga(a){var b=Ca.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ha(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(W.hasData(a)&&(f=W.access(a),g=W.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c<d;c++)r.event.add(b,e,j[e][c])}X.hasData(a)&&(h=X.access(a),i=r.extend({},h),X.set(b,i))}}function Ia(a,b){var c=b.nodeName.toLowerCase();"input"===c&&ja.test(a.type)?b.checked=a.checked:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}function Ja(a,b,c,d){b=g.apply([],b);var e,f,h,i,j,k,l=0,m=a.length,n=m-1,q=b[0],s=r.isFunction(q);if(s||m>1&&"string"==typeof q&&!o.checkClone&&Ba.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ja(f,b,c,d)});if(m&&(e=qa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(na(e,"script"),Fa),i=h.length;l<m;l++)j=e,l!==n&&(j=r.clone(j,!0,!0),i&&r.merge(h,na(j,"script"))),c.call(a[l],j,l);if(i)for(k=h[h.length-1].ownerDocument,r.map(h,Ga),l=0;l<i;l++)j=h[l],la.test(j.type||"")&&!W.access(j,"globalEval")&&r.contains(k,j)&&(j.src?r._evalUrl&&r._evalUrl(j.src):p(j.textContent.replace(Da,""),k))}return a}function Ka(a,b,c){for(var d,e=b?r.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||r.cleanData(na(d)),d.parentNode&&(c&&r.contains(d.ownerDocument,d)&&oa(na(d,"script")),d.parentNode.removeChild(d));return a}r.extend({htmlPrefilter:function(a){return a.replace(za,"<$1></$2>")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=na(h),f=na(a),d=0,e=f.length;d<e;d++)Ia(f[d],g[d]);if(b)if(c)for(f=f||na(a),g=g||na(h),d=0,e=f.length;d<e;d++)Ha(f[d],g[d]);else Ha(a,h);return g=na(h,"script"),g.length>0&&oa(g,!i&&na(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(U(c)){if(b=c[W.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[W.expando]=void 0}c[X.expando]&&(c[X.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ka(this,a,!0)},remove:function(a){return Ka(this,a)},text:function(a){return T(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.appendChild(a)}})},prepend:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(na(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return T(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!Aa.test(a)&&!ma[(ka.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c<d;c++)b=this[c]||{},1===b.nodeType&&(r.cleanData(na(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return Ja(this,arguments,function(b){var c=this.parentNode;r.inArray(this,a)<0&&(r.cleanData(na(this)),c&&c.replaceChild(b,this))},a)}}),r.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){r.fn[a]=function(a){for(var c,d=[],e=r(a),f=e.length-1,g=0;g<=f;g++)c=g===f?this:this.clone(!0),r(e[g])[b](c),h.apply(d,c.get());return this.pushStack(d)}});var La=/^margin/,Ma=new RegExp("^("+aa+")(?!px)[a-z%]+$","i"),Na=function(b){var c=b.ownerDocument.defaultView;return c&&c.opener||(c=a),c.getComputedStyle(b)};!function(){function b(){if(i){i.style.cssText="box-sizing:border-box;position:relative;display:block;margin:auto;border:1px;padding:1px;top:1%;width:50%",i.innerHTML="",ra.appendChild(h);var b=a.getComputedStyle(i);c="1%"!==b.top,g="2px"===b.marginLeft,e="4px"===b.width,i.style.marginRight="50%",f="4px"===b.marginRight,ra.removeChild(h),i=null}}var c,e,f,g,h=d.createElement("div"),i=d.createElement("div");i.style&&(i.style.backgroundClip="content-box",i.cloneNode(!0).style.backgroundClip="",o.clearCloneStyle="content-box"===i.style.backgroundClip,h.style.cssText="border:0;width:8px;height:0;top:0;left:-9999px;padding:0;margin-top:1px;position:absolute",h.appendChild(i),r.extend(o,{pixelPosition:function(){return b(),c},boxSizingReliable:function(){return b(),e},pixelMarginRight:function(){return b(),f},reliableMarginLeft:function(){return b(),g}}))}();function Oa(a,b,c){var d,e,f,g,h=a.style;return c=c||Na(a),c&&(g=c.getPropertyValue(b)||c[b],""!==g||r.contains(a.ownerDocument,a)||(g=r.style(a,b)),!o.pixelMarginRight()&&Ma.test(g)&&La.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f)),void 0!==g?g+"":g}function Pa(a,b){return{get:function(){return a()?void delete this.get:(this.get=b).apply(this,arguments)}}}var Qa=/^(none|table(?!-c[ea]).+)/,Ra=/^--/,Sa={position:"absolute",visibility:"hidden",display:"block"},Ta={letterSpacing:"0",fontWeight:"400"},Ua=["Webkit","Moz","ms"],Va=d.createElement("div").style;function Wa(a){if(a in Va)return a;var b=a[0].toUpperCase()+a.slice(1),c=Ua.length;while(c--)if(a=Ua[c]+b,a in Va)return a}function Xa(a){var b=r.cssProps[a];return b||(b=r.cssProps[a]=Wa(a)||a),b}function Ya(a,b,c){var d=ba.exec(b);return d?Math.max(0,d[2]-(c||0))+(d[3]||"px"):b}function Za(a,b,c,d,e){var f,g=0;for(f=c===(d?"border":"content")?4:"width"===b?1:0;f<4;f+=2)"margin"===c&&(g+=r.css(a,c+ca[f],!0,e)),d?("content"===c&&(g-=r.css(a,"padding"+ca[f],!0,e)),"margin"!==c&&(g-=r.css(a,"border"+ca[f]+"Width",!0,e))):(g+=r.css(a,"padding"+ca[f],!0,e),"padding"!==c&&(g+=r.css(a,"border"+ca[f]+"Width",!0,e)));return g}function $a(a,b,c){var d,e=Na(a),f=Oa(a,b,e),g="border-box"===r.css(a,"boxSizing",!1,e);return Ma.test(f)?f:(d=g&&(o.boxSizingReliable()||f===a.style[b]),"auto"===f&&(f=a["offset"+b[0].toUpperCase()+b.slice(1)]),f=parseFloat(f)||0,f+Za(a,b,c||(g?"border":"content"),d,e)+"px")}r.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=Oa(a,"opacity");return""===c?"1":c}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":"cssFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=r.camelCase(b),i=Ra.test(b),j=a.style;return i||(b=Xa(h)),g=r.cssHooks[b]||r.cssHooks[h],void 0===c?g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:j[b]:(f=typeof c,"string"===f&&(e=ba.exec(c))&&e[1]&&(c=fa(a,b,e),f="number"),null!=c&&c===c&&("number"===f&&(c+=e&&e[3]||(r.cssNumber[h]?"":"px")),o.clearCloneStyle||""!==c||0!==b.indexOf("background")||(j[b]="inherit"),g&&"set"in g&&void 0===(c=g.set(a,c,d))||(i?j.setProperty(b,c):j[b]=c)),void 0)}},css:function(a,b,c,d){var e,f,g,h=r.camelCase(b),i=Ra.test(b);return i||(b=Xa(h)),g=r.cssHooks[b]||r.cssHooks[h],g&&"get"in g&&(e=g.get(a,!0,c)),void 0===e&&(e=Oa(a,b,d)),"normal"===e&&b in Ta&&(e=Ta[b]),""===c||c?(f=parseFloat(e),c===!0||isFinite(f)?f||0:e):e}}),r.each(["height","width"],function(a,b){r.cssHooks[b]={get:function(a,c,d){if(c)return!Qa.test(r.css(a,"display"))||a.getClientRects().length&&a.getBoundingClientRect().width?$a(a,b,d):ea(a,Sa,function(){return $a(a,b,d)})},set:function(a,c,d){var e,f=d&&Na(a),g=d&&Za(a,b,d,"border-box"===r.css(a,"boxSizing",!1,f),f);return g&&(e=ba.exec(c))&&"px"!==(e[3]||"px")&&(a.style[b]=c,c=r.css(a,b)),Ya(a,c,g)}}}),r.cssHooks.marginLeft=Pa(o.reliableMarginLeft,function(a,b){if(b)return(parseFloat(Oa(a,"marginLeft"))||a.getBoundingClientRect().left-ea(a,{marginLeft:0},function(){return a.getBoundingClientRect().left}))+"px"}),r.each({margin:"",padding:"",border:"Width"},function(a,b){r.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];d<4;d++)e[a+ca[d]+b]=f[d]||f[d-2]||f[0];return e}},La.test(a)||(r.cssHooks[a+b].set=Ya)}),r.fn.extend({css:function(a,b){return T(this,function(a,b,c){var d,e,f={},g=0;if(Array.isArray(b)){for(d=Na(a),e=b.length;g<e;g++)f[b[g]]=r.css(a,b[g],!1,d);return f}return void 0!==c?r.style(a,b,c):r.css(a,b)},a,b,arguments.length>1)}});function _a(a,b,c,d,e){return new _a.prototype.init(a,b,c,d,e)}r.Tween=_a,_a.prototype={constructor:_a,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=_a.propHooks[this.prop];return a&&a.get?a.get(this):_a.propHooks._default.get(this)},run:function(a){var b,c=_a.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):_a.propHooks._default.set(this),this}},_a.prototype.init.prototype=_a.prototype,_a.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},_a.propHooks.scrollTop=_a.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=_a.prototype.init,r.fx.step={};var ab,bb,cb=/^(?:toggle|show|hide)$/,db=/queueHooks$/;function eb(){bb&&(d.hidden===!1&&a.requestAnimationFrame?a.requestAnimationFrame(eb):a.setTimeout(eb,r.fx.interval),r.fx.tick())}function fb(){return a.setTimeout(function(){ab=void 0}),ab=r.now()}function gb(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ca[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function hb(a,b,c){for(var d,e=(kb.tweeners[b]||[]).concat(kb.tweeners["*"]),f=0,g=e.length;f<g;f++)if(d=e[f].call(c,b,a))return d}function ib(a,b,c){var d,e,f,g,h,i,j,k,l="width"in b||"height"in b,m=this,n={},o=a.style,p=a.nodeType&&da(a),q=W.get(a,"fxshow");c.queue||(g=r._queueHooks(a,"fx"),null==g.unqueued&&(g.unqueued=0,h=g.empty.fire,g.empty.fire=function(){g.unqueued||h()}),g.unqueued++,m.always(function(){m.always(function(){g.unqueued--,r.queue(a,"fx").length||g.empty.fire()})}));for(d in b)if(e=b[d],cb.test(e)){if(delete b[d],f=f||"toggle"===e,e===(p?"hide":"show")){if("show"!==e||!q||void 0===q[d])continue;p=!0}n[d]=q&&q[d]||r.style(a,d)}if(i=!r.isEmptyObject(b),i||!r.isEmptyObject(n)){l&&1===a.nodeType&&(c.overflow=[o.overflow,o.overflowX,o.overflowY],j=q&&q.display,null==j&&(j=W.get(a,"display")),k=r.css(a,"display"),"none"===k&&(j?k=j:(ia([a],!0),j=a.style.display||j,k=r.css(a,"display"),ia([a]))),("inline"===k||"inline-block"===k&&null!=j)&&"none"===r.css(a,"float")&&(i||(m.done(function(){o.display=j}),null==j&&(k=o.display,j="none"===k?"":k)),o.display="inline-block")),c.overflow&&(o.overflow="hidden",m.always(function(){o.overflow=c.overflow[0],o.overflowX=c.overflow[1],o.overflowY=c.overflow[2]})),i=!1;for(d in n)i||(q?"hidden"in q&&(p=q.hidden):q=W.access(a,"fxshow",{display:j}),f&&(q.hidden=!p),p&&ia([a],!0),m.done(function(){p||ia([a]),W.remove(a,"fxshow");for(d in n)r.style(a,d,n[d])})),i=hb(p?q[d]:0,d,m),d in q||(q[d]=i.start,p&&(i.end=i.start,i.start=0))}}function jb(a,b){var c,d,e,f,g;for(c in a)if(d=r.camelCase(c),e=b[d],f=a[c],Array.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=r.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function kb(a,b,c){var d,e,f=0,g=kb.prefilters.length,h=r.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=ab||fb(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;g<i;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),f<1&&i?c:(i||h.notifyWith(a,[j,1,0]),h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:r.extend({},b),opts:r.extend(!0,{specialEasing:{},easing:r.easing._default},c),originalProperties:b,originalOptions:c,startTime:ab||fb(),duration:c.duration,tweens:[],createTween:function(b,c){var d=r.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;c<d;c++)j.tweens[c].run(1);return b?(h.notifyWith(a,[j,1,0]),h.resolveWith(a,[j,b])):h.rejectWith(a,[j,b]),this}}),k=j.props;for(jb(k,j.opts.specialEasing);f<g;f++)if(d=kb.prefilters[f].call(j,a,k,j.opts))return r.isFunction(d.stop)&&(r._queueHooks(j.elem,j.opts.queue).stop=r.proxy(d.stop,d)),d;return r.map(k,hb,j),r.isFunction(j.opts.start)&&j.opts.start.call(a,j),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always),r.fx.timer(r.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j}r.Animation=r.extend(kb,{tweeners:{"*":[function(a,b){var c=this.createTween(a,b);return fa(c.elem,a,ba.exec(b),c),c}]},tweener:function(a,b){r.isFunction(a)?(b=a,a=["*"]):a=a.match(L);for(var c,d=0,e=a.length;d<e;d++)c=a[d],kb.tweeners[c]=kb.tweeners[c]||[],kb.tweeners[c].unshift(b)},prefilters:[ib],prefilter:function(a,b){b?kb.prefilters.unshift(a):kb.prefilters.push(a)}}),r.speed=function(a,b,c){var d=a&&"object"==typeof a?r.extend({},a):{complete:c||!c&&b||r.isFunction(a)&&a,duration:a,easing:c&&b||b&&!r.isFunction(b)&&b};return r.fx.off?d.duration=0:"number"!=typeof d.duration&&(d.duration in r.fx.speeds?d.duration=r.fx.speeds[d.duration]:d.duration=r.fx.speeds._default),null!=d.queue&&d.queue!==!0||(d.queue="fx"),d.old=d.complete,d.complete=function(){r.isFunction(d.old)&&d.old.call(this),d.queue&&r.dequeue(this,d.queue)},d},r.fn.extend({fadeTo:function(a,b,c,d){return this.filter(da).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=r.isEmptyObject(a),f=r.speed(b,c,d),g=function(){var b=kb(this,r.extend({},a),f);(e||W.get(this,"finish"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return"string"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,e=null!=a&&a+"queueHooks",f=r.timers,g=W.get(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&db.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));!b&&c||r.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||"fx"),this.each(function(){var b,c=W.get(this),d=c[a+"queue"],e=c[a+"queueHooks"],f=r.timers,g=d?d.length:0;for(c.finish=!0,r.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;b<g;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),r.each(["toggle","show","hide"],function(a,b){var c=r.fn[b];r.fn[b]=function(a,d,e){return null==a||"boolean"==typeof a?c.apply(this,arguments):this.animate(gb(b,!0),a,d,e)}}),r.each({slideDown:gb("show"),slideUp:gb("hide"),slideToggle:gb("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){r.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),r.timers=[],r.fx.tick=function(){var a,b=0,c=r.timers;for(ab=r.now();b<c.length;b++)a=c[b],a()||c[b]!==a||c.splice(b--,1);c.length||r.fx.stop(),ab=void 0},r.fx.timer=function(a){r.timers.push(a),r.fx.start()},r.fx.interval=13,r.fx.start=function(){bb||(bb=!0,eb())},r.fx.stop=function(){bb=null},r.fx.speeds={slow:600,fast:200,_default:400},r.fn.delay=function(b,c){return b=r.fx?r.fx.speeds[b]||b:b,c=c||"fx",this.queue(c,function(c,d){var e=a.setTimeout(c,b);d.stop=function(){a.clearTimeout(e)}})},function(){var a=d.createElement("input"),b=d.createElement("select"),c=b.appendChild(d.createElement("option"));a.type="checkbox",o.checkOn=""!==a.value,o.optSelected=c.selected,a=d.createElement("input"),a.value="t",a.type="radio",o.radioValue="t"===a.value}();var lb,mb=r.expr.attrHandle;r.fn.extend({attr:function(a,b){return T(this,r.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?lb:void 0)),void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b),
null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&B(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(L);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),lb={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=mb[b]||r.find.attr;mb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=mb[g],mb[g]=e,e=null!=c(a,b,d)?g:null,mb[g]=f),e}});var nb=/^(?:input|select|textarea|button)$/i,ob=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return T(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):nb.test(a.nodeName)||ob.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function pb(a){var b=a.match(L)||[];return b.join(" ")}function qb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,qb(this)))});if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,qb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,qb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(L)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=qb(this),b&&W.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":W.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+pb(qb(c))+" ").indexOf(b)>-1)return!0;return!1}});var rb=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":Array.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(rb,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:pb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d<i;d++)if(c=e[d],(c.selected||d===f)&&!c.disabled&&(!c.parentNode.disabled||!B(c.parentNode,"optgroup"))){if(b=r(c).val(),g)return b;h.push(b)}return h},set:function(a,b){var c,d,e=a.options,f=r.makeArray(b),g=e.length;while(g--)d=e[g],(d.selected=r.inArray(r.valHooks.option.get(d),f)>-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(Array.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var sb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!sb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,sb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(W.get(h,"events")||{})[b.type]&&W.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&U(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!U(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=W.access(d,b);e||d.addEventListener(a,c,!0),W.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=W.access(d,b)-1;e?W.access(d,b,e):(d.removeEventListener(a,c,!0),W.remove(d,b))}}});var tb=a.location,ub=r.now(),vb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var wb=/\[\]$/,xb=/\r?\n/g,yb=/^(?:submit|button|image|reset|file)$/i,zb=/^(?:input|select|textarea|keygen)/i;function Ab(a,b,c,d){var e;if(Array.isArray(b))r.each(b,function(b,e){c||wb.test(a)?d(a,e):Ab(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)Ab(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(Array.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)Ab(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&zb.test(this.nodeName)&&!yb.test(a)&&(this.checked||!ja.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:Array.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(xb,"\r\n")}}):{name:b.name,value:c.replace(xb,"\r\n")}}).get()}});var Bb=/%20/g,Cb=/#.*$/,Db=/([?&])_=[^&]*/,Eb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Fb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Gb=/^(?:GET|HEAD)$/,Hb=/^\/\//,Ib={},Jb={},Kb="*/".concat("*"),Lb=d.createElement("a");Lb.href=tb.href;function Mb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(L)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Nb(a,b,c,d){var e={},f=a===Jb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Ob(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Pb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Qb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:tb.href,type:"GET",isLocal:Fb.test(tb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Kb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Ob(Ob(a,r.ajaxSettings),b):Ob(r.ajaxSettings,a)},ajaxPrefilter:Mb(Ib),ajaxTransport:Mb(Jb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Eb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||tb.href)+"").replace(Hb,tb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(L)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Lb.protocol+"//"+Lb.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Nb(Ib,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Gb.test(o.type),f=o.url.replace(Cb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(Bb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(vb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Db,"$1"),n=(vb.test(f)?"&":"?")+"_="+ub++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Kb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Nb(Jb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Pb(o,y,d)),v=Qb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Rb={0:200,1223:204},Sb=r.ajaxSettings.xhr();o.cors=!!Sb&&"withCredentials"in Sb,o.ajax=Sb=!!Sb,r.ajaxTransport(function(b){var c,d;if(o.cors||Sb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Rb[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r("<script>").prop({charset:a.scriptCharset,src:a.url}).on("load error",c=function(a){b.remove(),c=null,a&&f("error"===a.type?404:200,a.type)}),d.head.appendChild(b[0])},abort:function(){c&&c()}}}});var Tb=[],Ub=/(=)\?(?=&|$)|\?\?/;r.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=Tb.pop()||r.expando+"_"+ub++;return this[a]=!0,a}}),r.ajaxPrefilter("json jsonp",function(b,c,d){var e,f,g,h=b.jsonp!==!1&&(Ub.test(b.url)?"url":"string"==typeof b.data&&0===(b.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ub.test(b.data)&&"data");if(h||"jsonp"===b.dataTypes[0])return e=b.jsonpCallback=r.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,h?b[h]=b[h].replace(Ub,"$1"+e):b.jsonp!==!1&&(b.url+=(vb.test(b.url)?"&":"?")+b.jsonp+"="+e),b.converters["script json"]=function(){return g||r.error(e+" was not called"),g[0]},b.dataTypes[0]="json",f=a[e],a[e]=function(){g=arguments},d.always(function(){void 0===f?r(a).removeProp(e):a[e]=f,b[e]&&(b.jsonpCallback=c.jsonpCallback,Tb.push(e)),g&&r.isFunction(f)&&f(g[0]),g=f=void 0}),"script"}),o.createHTMLDocument=function(){var a=d.implementation.createHTMLDocument("").body;return a.innerHTML="<form></form><form></form>",2===a.childNodes.length}(),r.parseHTML=function(a,b,c){if("string"!=typeof a)return[];"boolean"==typeof b&&(c=b,b=!1);var e,f,g;return b||(o.createHTMLDocument?(b=d.implementation.createHTMLDocument(""),e=b.createElement("base"),e.href=d.location.href,b.head.appendChild(e)):b=d),f=C.exec(a),g=!c&&[],f?[b.createElement(f[1])]:(f=qa([a],b,g),g&&g.length&&r(g).remove(),r.merge([],f.childNodes))},r.fn.load=function(a,b,c){var d,e,f,g=this,h=a.indexOf(" ");return h>-1&&(d=pb(a.slice(h)),a=a.slice(0,h)),r.isFunction(b)?(c=b,b=void 0):b&&"object"==typeof b&&(e="POST"),g.length>0&&r.ajax({url:a,type:e||"GET",dataType:"html",data:b}).done(function(a){f=arguments,g.html(d?r("<div>").append(r.parseHTML(a)).find(d):a)}).always(c&&function(a,b){g.each(function(){c.apply(this,f||[a.responseText,b,a])})}),this},r.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(a,b){r.fn[b]=function(a){return this.on(b,a)}}),r.expr.pseudos.animated=function(a){return r.grep(r.timers,function(b){return a===b.elem}).length},r.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=r.css(a,"position"),l=r(a),m={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=r.css(a,"top"),i=r.css(a,"left"),j=("absolute"===k||"fixed"===k)&&(f+i).indexOf("auto")>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),r.isFunction(b)&&(b=b.call(a,c,r.extend({},h))),null!=b.top&&(m.top=b.top-h.top+g),null!=b.left&&(m.left=b.left-h.left+e),"using"in b?b.using.call(a,m):l.css(m)}},r.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){r.offset.setOffset(this,a,b)});var b,c,d,e,f=this[0];if(f)return f.getClientRects().length?(d=f.getBoundingClientRect(),b=f.ownerDocument,c=b.documentElement,e=b.defaultView,{top:d.top+e.pageYOffset-c.clientTop,left:d.left+e.pageXOffset-c.clientLeft}):{top:0,left:0}},position:function(){if(this[0]){var a,b,c=this[0],d={top:0,left:0};return"fixed"===r.css(c,"position")?b=c.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),B(a[0],"html")||(d=a.offset()),d={top:d.top+r.css(a[0],"borderTopWidth",!0),left:d.left+r.css(a[0],"borderLeftWidth",!0)}),{top:b.top-d.top-r.css(c,"marginTop",!0),left:b.left-d.left-r.css(c,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent;while(a&&"static"===r.css(a,"position"))a=a.offsetParent;return a||ra})}}),r.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,b){var c="pageYOffset"===b;r.fn[a]=function(d){return T(this,function(a,d,e){var f;return r.isWindow(a)?f=a:9===a.nodeType&&(f=a.defaultView),void 0===e?f?f[b]:a[d]:void(f?f.scrollTo(c?f.pageXOffset:e,c?e:f.pageYOffset):a[d]=e)},a,d,arguments.length)}}),r.each(["top","left"],function(a,b){r.cssHooks[b]=Pa(o.pixelPosition,function(a,c){if(c)return c=Oa(a,b),Ma.test(c)?r(a).position()[b]+"px":c})}),r.each({Height:"height",Width:"width"},function(a,b){r.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){r.fn[d]=function(e,f){var g=arguments.length&&(c||"boolean"!=typeof e),h=c||(e===!0||f===!0?"margin":"border");return T(this,function(b,c,e){var f;return r.isWindow(b)?0===d.indexOf("outer")?b["inner"+a]:b.document.documentElement["client"+a]:9===b.nodeType?(f=b.documentElement,Math.max(b.body["scroll"+a],f["scroll"+a],b.body["offset"+a],f["offset"+a],f["client"+a])):void 0===e?r.css(b,c,h):r.style(b,c,e,h)},b,g?e:void 0,g)}})}),r.fn.extend({bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}}),r.holdReady=function(a){a?r.readyWait++:r.ready(!0)},r.isArray=Array.isArray,r.parseJSON=JSON.parse,r.nodeName=B,"function"==typeof define&&define.amd&&define("jquery",[],function(){return r});var Vb=a.jQuery,Wb=a.$;return r.noConflict=function(b){return a.$===r&&(a.$=Wb),b&&a.jQuery===r&&(a.jQuery=Vb),r},b||(a.jQuery=a.$=r),r});
Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View File
File diff suppressed because it is too large Load Diff
+717
View File
@@ -0,0 +1,717 @@
# Taffy
The REST Web Service framework for ColdFusion and Railo
<a id="download" href="https://github.com/atuttle/Taffy/archive/v2.0.0.zip">Download Taffy 2.0.0.zip</a>
- [Release Notes](https://github.com/atuttle/Taffy/releases/tag/v2.0.0)
- [GitHub](https://github.com/atuttle/Taffy)
- Join us in the **#taffy** chat room on the [CFML Slack](https://cfml-slack.herokuapp.com/)
## Quickstart: Your First API
**Application.cfc:**
```js
component extends="taffy.core.api" {}
```
**index.cfm:**
```html
<!-- this space left intentionally blank -->
```
**/resources/hello.cfc:**
```js
component extends="taffy.core.resource" taffy_uri="/hello" {
function get(){
return representationOf(['hello','world']);
}
}
```
Congratulations, you've just written your first API. Didn't think it could be so simple, did you? Indeed, [you can fit an entire Taffy API into a single tweet](https://twitter.com/cf_taffy/statuses/327415972581486592).
Point your browser to the empty **index.cfm** file you created, and have a look at the dashboard. Click the "hello" row to expand it, and click the **Send** button to make a REST request. The results of your request are displayed below.
The response includes a **status code** and **status text**, as well as other **headers**, and the **response body** (if applicable). Taffy also shows you the amount of time the request took.
### Getting Started, with more details
Here's a [Getting Started guide](https://github.com/atuttle/Taffy/wiki/Getting-Started) that goes into a little bit more detail, if the above example wasn't enough.
## Installation Options
### Global /taffy Mapping
Best practice would be to keep Taffy _out of your web root_. This is also (almost) the easiest install method. Instead of putting taffy in the web root, use a global mapping (set in the CF Administrator) pointing `/taffy` to wherever you have it saved.
### /taffy in Web Root
Just put the /taffy folder at the root of your domain. Boom, done.
### Sub-folder
If you aren't allowed to create a global mapping, or for some other reason want to, you can install Taffy as a sub-folder of your API. (NB: This would also allow you to use multiple versions of Taffy on the same CF Instance.)
Using sub-folders requires the use of Application-Specific Mappings (introduced in Adobe ColdFusion 8). Start by creating this directory structure:
```
/path-to-your-api
├── Application.cfc
├── index.cfm
├─┬ /taffy
│ ├── /bonus
│ ├── /core
│ └── /dashboard
└─┬ /resources
├── ...
└── someResource.cfc
```
You can see that the taffy folder is a sibling to Application.cfc. This allows Application.cfc to use relative paths to extend `taffy.core.api`.
Next, if your Application.cfc and `/resources/` folder aren't in the web-root (e.g. they're inside something like `/api/`) then you'll need to add an [application-specific mapping](http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=appFramework_04.html) for `/resources` so that Taffy can find your resources to initialize the routes.
```js
this.mappings["/resources"] = expandPath("./resources");
```
You'll also need to add a mapping for `/taffy` so that the resources can extend `taffy.core.resource` (since the taffy folder isn't a child of the resources folder):
```js
this.mappings["/taffy"] = expandPath("./taffy");
```
## Authentication and Security
If your authentication and security requirements are simple, you may find the combination of SSL and HTTP Basic Auth to be sufficient. I would advise you not to use Basic Auth without SSL, as it is then easily sniffed. If you're utilizing Basic Auth, Taffy 1.3 added [getBasicAuthCredentials()](#getbasicauthcredentials).
If you're after something a bit more complex, but still short of OAuth, I've written an article covering [Advanced Authentication with Taffy](https://adamtuttle.codes/blog/2013/advanced-authentication-with-taffy-apis/).
Implementing OAuth is something I would like to document, but it is a fairly large topic and I haven't had the time yet. Pull requests welcome.
## Railo Idiosyncrasies
### Custom HTTP Status Messages
Taffy allows for setting the HTTP status message using [.withStatus()](#withstatus), such as:
```js
return representationOf({...}).withStatus(403, "Not Authorized");
```
Railo's default setup on Tomcat doesn't allow changing the Status Text header value; so you might use `.withStatus(403, "Account Past Due")` but Tomcat changes this back to `403 Not Authorized`.
You can either work around this by passing status information in the response body, or alternately changing **catalina.properties** (Tomcat config) to include the line:
```conf
org.apache.coyote.USE_CUSTOM_STATUS_MSG_IN_HEADER=true
```
[Hat tip to Jean-Bernard van Zuylen](https://github.com/atuttle/Taffy/issues/121#issuecomment-23410624)
### PUT requests & FORM scope
Prior to versions 4.1.1.002 and 4.2 of Railo, a PUT request would populate the FORM scope. This probably doesn't cause any issues with API's, but did cause a pair of unit tests to fail. (put_body_is_mime_content, and put_body_is_url_encoded_params) This is safe to ignore. Also, upgrade, man!
[Hat tip to Jean-Bernard van Zuylen](https://github.com/atuttle/Taffy/issues/121#issuecomment-23410624)
## More Guides
Some guides are too broad for this document. For your benefit, they are linked here:
- [Migration Guide](https://github.com/atuttle/Taffy/wiki/Migration-Guide) - Notes on upgrading from 1.3.x to 2.0.x
## Configuration Reference
### Configuration via Metadata
Metadata is used to apply configuration in a concise and elegant manor, where necessity and possibility intersect.
#### In Resources
Resources are CFCs that interpret a request and provide or update the requested data.
##### taffy:uri
The **taffy:uri** property applies to the `<cfcomponent>` tag or the `component{}` definition. It configures which URIs the CFC will respond to. There is no limit to the number of tokens that can be used in a single URI, as long as they are not consecutive and are delimited by at least one character (like a slash). Examples:
```xml
<cfcomponent taffy:uri="/artist/{artistId}">
</cfcomponent>
```
or
```js
component taffy_uri="/artist/{artistId}" {
}
```
The following example is _invalid_, because the tokens are not separated by (at least) a slash:<br/>
```
taffy:uri="/artist/{artistId}{artistName}"
```
##### Tokens
Tokens in URIs define the parts of the URI that are dynamic. They are identified by curly-brackets: `{tokenName}` and are passed **by name** to all functions that handle the request. The `{foo}` token's value will be passed to the `foo` argument of the associated method.
##### URI Matching Order
As of Taffy 1.3, URIs are processed into a sorted array on API startup, and searched in order for every request, such that `/artists/list` will match before `/artists/{id}`. You should design your API URI's accordingly. Pay special attention to placement and possible values for URI tokens. If a possible value is the same as a static URI, consider changing URI formatting. For example, if we had an artist with id "list", then having the static URI `/artists/list` would prevent the `/artists/{id}` URI from ever matching when the intention is to get the individual artist record for artist with id "list."
##### taffy:verb
By convention, resources will automatically map the 4 primary HTTP REST verbs -- GET, PUT, POST, DELETE -- to CFC methods with the same name. For extended verbs -- OPTIONS, HEAD -- or if you want to map one of the primary verbs to a method with a different name, you can use the `taffy:verb` metadata property on the method to specify the verb it should respond to. Examples:
```xml
<cffunction name="getUser" taffy:verb="get">
<cfargument name="userId" type="numeric" />
</cffunction>
```
or
```js
function getUser( numeric userId ) taffy_verb="get" {
}
```
#### In Representation Classes
Representation classes are used to take the data provided by a resource and serialize it into a format usable by the web service consumer. A single Representation Class is capable of serializing native data objects (strings, numbers, queries, structures, arrays, etc) into 1 or more formats. Typical formats include JSON, XML, or YAML, but are not limited.
##### taffy:mime
By convention, the mime-types supported by your API are determined by the method names in your default representation class. (The included default representation class supports only JSON.) Each `getAsX` method in your representation class describes a new mime type, defined in two parts: The X portion of the method name determines the extension of the mime type -- which can be appended to the URI as if it were a file, as in: `/artists/42.json` which would use `getAsJson`. Taffy will also return a content-type header with the content type that you supply in the `taffy:mime` metadata property of the `getAsX` method. Typically, this is something like "application/json" or "application/xml". Examples:
```xml
<cffunction name="getAsJson" taffy:mime="application/json">
</cffunction>
```
or
```js
function getAsJson() taffy_mime="application/json" {
}
```
##### taffy:default
When your API supports more than one data format (i.e. json and xml), you must set one as the default. You do this with the `taffy:default` property, which expects a boolean value of either TRUE or FALSE. The default is FALSE, and you never need to include `taffy:default="false"`. You only need to include `taffy:default="true"` on one method -- the one that should be the default. Examples:
```xml
<cffunction name="getAsJson" taffy:mime="application/json" taffy:default="true">
</cffunction>
<cffunction name="getAsXml" taffy:mime="application/xml">
</cffunction>
```
or
```js
function getAsJson() taffy_mime="application/json" taffy_default="true" {}
function getAsXml() taffy_mime="application/xml" {}
```
### variables.framework settings
Default values:
```js
variables.framework = {
reloadKey = "reload",
reloadPassword = "true",
reloadOnEveryRequest = false,
endpointURLParam = "endpoint",
representationClass = "taffy.core.nativeJsonRepresentation",
dashboardKey = "dashboard",
disableDashboard = false,
disabledDashboardRedirect = "",
jsonp = false,
unhandledPaths = "/flex2gateway",
allowCrossDomain = false,
globalHeaders = structNew(),
debugKey = "debug",
useEtags = false,
returnExceptionsAsJson = true,
exceptionLogAdapter = "taffy.bonus.LogToEmail",
exceptionLogAdapterConfig = {
emailFrom = "api-error@yourdomain.com",
emailTo = "you@yourdomain.com",
emailSubj = "Exception Trapped in API",
emailType = "html"
},
beanFactory = "",
environments = {}
};
```
#### reloadKey
**Available in:** Taffy 1.2+<br/>
**Type:** String<br/>
**Default:** "reload"<br/>
**Description:** Name of the url parameter that requests the framework to be reloaded. Used in combination with the reload password (see: reloadPassword), the framework will re-initialize itself. During re-initialization, all configuration settings are re-applied and all cached objects are cleared and reloaded. If the value of the key does not match the reload password, a reload will not be performed. This allows you to set a secret password to restrict control of reloading your API to trusted parties.
#### reloadPassword
**Available in:** Taffy 1.2+<br/>
**Type:** String<br/>
**Default:** "true"<br/>
**Description:** Accepted value of the url parameter that requests the framework to be reloaded. Used in combination with the reload key (see: reloadKey), the framework will re-initialize itself. During re-initialization, all configuration settings are re-applied and all cached objects are cleared and reloaded. If the value of the key does not match the reload password, a reload will not be performed. This allows you to set a secret password to restrict control of reloading your API to trusted parties.
#### reloadOnEveryRequest
**Available in:** Taffy 1.2+<br/>
**Type:** Boolean<br/>
**Default:** False<br/>
**Description:** Flag that indicates whether Taffy should reload cached values and configuration on every request. Useful in development; set to FALSE in production.
#### endpointURLParam
**Available in:** Taffy 1.3+<br/>
**Type:** String<br/>
**Default:** "endpoint"<br/>
**Description:** The query-string parameter name that can optionally be used to specify URI. Until now, URI formatting has been required to be index.cfm/URI; this parameter allows you to use index.cfm?endpoint=/URI. This setting (endpointURLParam) allows you to change the default parameter name of "endpoint" to something custom.
#### representationClass
**Available in:** Taffy 1.2+<br/>
**Type:** String<br/>
**Default:** "taffy.core.nativeJsonRepresentation"<br/>
**Description:** The CFC dot-notation path, or bean name, of the representation class that your API will use to serialize returned data for the client.
#### dashboardKey
**Available in:** Taffy 1.2+<br/>
**Deprecated **in: Taffy 1.3+<br/>
**Type:** String<br/>
**Default:** "dashboard"
Description: Name of the url parameter that displays the dashboard. The dashboard displays resources that your API is aware of, generates documentation about your API based on hint attributes, and contains a mock client to make testing your API easy.
#### disableDashboard
**Available in:** Taffy 1.2+<br/>
**Type:** Boolean<br/>
**Default:** False<br/>
**Description:** Whether or not Taffy will allow the dashboard to be displayed. If set to true, the dashboard key is simply ignored. You may wish to disable the dashboard in production, depending on whether or not you want customers/clients to be able to see it.
#### disabledDashboardRedirect
**Available in:** Taffy 1.3+<br/>
**Type:** String<br/>
**Default:** ""<br/>
**Description:** URL to which Taffy should redirect (302) the client/browser if the dashboard is disabled. If the dashboard is disabled and this value is blank, a simple 403 Forbidden response is sent instead.
#### jsonp
**Available in:** Taffy 2.0+<br/>
**Type:** Boolean/String<br/>
**Default:** false<br/>
**Description:** When false, JSONP is disabled. To enable it, change the value to a string, such as "callback". The value you specify will be the query parameter in which Taffy expects to find the JSONP callback name. Note: JSONP only works for GET requests (by design!)
#### unhandledPaths
**Available in:** Taffy 1.2+<br/>
**Type:** String (Comma-delimited list)<br/>
**Default:** "/flex2gateway"<br/>
**Description:** Set a list of paths (usually subfolders of the API) that you do not want Taffy to interfere with. Unless listed here, Taffy takes over the request lifecycle and does not execute the requested ColdFusion template.
#### allowCrossDomain
**Available in:** Taffy 1.2+<br/>
**Type:** Boolean<br/>
**Default:** False<br/>
**Description:** Whether or not to allow cross-domain access to your API. Turning this on adds the following headers:
```xml
<cfheader name="Access-Control-Allow-Origin" value="*" />
<cfheader name="Access-Control-Allow-Methods" value="#allowedVerbs#" />
<cfheader name="Access-Control-Allow-Headers" value="Content-Type" />
```
The allowed verbs, of course, are the ones allowed by the requested resource, as well as OPTIONS.
#### globalHeaders
**Available in:** Taffy 1.2+<br/>
**Type:** Structure<br/>
**Default:** {}<br/>
**Description:** A structure where each key is the name of a header you want to return, such as "X-MY-HEADER" and the structure value is the header value.
Global headers are static. You set them on application initialization and they do not change. If you need dynamic headers, you can add them to each response at runtime using withHeaders().
#### debugKey
**Available in:** Taffy 1.2+<br/>
**Type:** String<br/>
**Default:** "debug"<br/>
**Description:** Name of the url parameter that enables CF Debug Output.
#### useEtags
**Available in:** Taffy 1.3+<br/>
**Type:** Boolean<br/>
**Default:** False<br/>
**Description:** Enable the use of HTTP ETags for caching purposes. Taffy will automatically handle both sending the server ETag value and detecting client supplied ETags (via the If-None-Match header) for you; simply turn this setting on.
**NOTE FOR RAILO USERS:** While it will not cause errors, the underlying Java code used in this feature was improperly implemented prior to Railo 4.0.? and this could result in your result data being sent as if it were changed when it in fact has not. (I'm not sure which Railo point release will include the fix. The latest as of this writing is version 4.0.2, and does not include it.) Adobe ColdFusion is unaffected.
#### returnExceptionsAsJson
**Available in:** Taffy 1.2+<br/>
**Type:** Boolean<br/>
**Default:** true<br/>
**Description:** When an error occurs that is not otherwise handled, this option tells Taffy to attempt to format the error information as JSON and return that (regardless of the requested return format).
#### exceptionLogAdapter
**Available in:** Taffy 1.2+<br/>
**Type:** String<br/>
**Default:** "taffy.bonus.LogToEmail"<br/>
**Description:** CFC dot-notation path to the exception logging adapter you want to use. Default adapter simply emails all exceptions. See Exception Logging Adapters for more details.
#### exceptionLogAdapterConfig
**Available in:** Taffy 1.2+<br/>
**Type:** Any<br/>
**Default:** See defaults [here](#variablesframework-settings)<br/>
**Description:** Configuration that your chosen logging adapter requires. Can be any data type. See Exception Logging Adapters for more details.
#### beanFactory
**Available in:** Taffy 1.2+<br/>
**Type:** Object Instance<br/>
**Default:** ""<br/>
**Description:** Already instantiated and cached (e.g. in Application scope) object instance of your external bean factory. Not required in order to use Taffy's built-in factory.
#### environments
**Available in:** Taffy 1.3+<br/>
**Type:** Structure<br/>
**Default:** {}<br/>
**Description:** Environment-specific overrides to any framework settings. Applied after general variables.framework settings, and after configureTaffy() has been called. See [getEnvironment()](#getenvironment) for more details.
## Index of API Methods
### Application.cfc
#### getPath()
**Parameters:** _(none)_
This method is provided as an extension point. On Adobe ColdFusion ("ACF") 9, installed in standard entire-server mode, no change should be necessary. However, if ACF is installed on another JEE app server (i.e. Tomcat, Glassfish, etc), or on JRun but using an EAR/WAR setup, then you may need to override this method to make Taffy work on your server. See [[GetPath Setups]] for more information.
#### getBasicAuthCredentials()
**Parameters:** _(none)_
**Added in Taffy 1.3.** This method returns a structure with two keys: `username`, and `password`. When the client does not provide HTTP Basic Auth credentials, the username and password keys will be blank. When the client does provide them, the values will be available in these keys.
This method is only available inside your Application.cfc. If, for example, you need access to the username in a resource, you can use this method inside onTaffyRequest and add the username to the requestArguments structure.
#### getBeanFactory()
**Parameters:** _(none)_
Returns whatever bean factory you may have set into Taffy, if any. If using the `/resources` folder, it returns Taffy's built-in factory.
#### newRepresentation()
**Use it inside:** onTaffyRequest<br/>
**Parameters:**
- **class (string)** - the dot-notation cfc path (or bean id if managing the class with a bean factory, including the built-in factory) for the representation class. Optional. Default value is the defined representation class. If you're using the framework default representation class, it's `nativeJsonRepresentation`.
Use this method inside onTaffyRequest when you want to _abort_ the request with some specific message or data. Use the `setData` method on the returned representation class to put your data/message into it before returning it.
#### getEnvironment()
**Parameters:** _(none)_
Taffy calls this method during initialization to determine in which configured environment, if any, it is executing. Overriding it is optional. Use whatever methodology you like to determine the result (e.g. hostname, reading a file or registry setting, etc.), and simply return, as string, the name of the current environment.
The returned value will be used to load environment-specific configuration. For example, if you have the following code in your Application.cfc, then the dashboard will be disabled in production:
```js
variables.framework = {
disableDashboard = false
,environments = {
production = {
disableDashboard = true
}
}
};
function getEnvironment(){
if (...){
return "production";
}else{
return "development";
}
}
```
#### onTaffyRequest()
**Parameters:**
- **verb (string)** - The HTTP request verb provided by the client
- **cfc (string)** - The CFC name (minus ".cfc") that would handle the request. (Bean Name, if using an external bean factory.)
- **requestArguments (struct)** - A structure containing all of the arguments of the request, including tokens from the URI as well as any query string parameters.
- **mimeExt (string)** - The mime extension (e.g. "json" - NOT the full mime type, e.g. "application/json")
- **headers (struct)** - A structure containing each header from the request, as sent by the client.
This method is optional, and allows you to inspect and potentially abort an API request in a way that adheres to the HTTP specification. If you choose not to override it (by implementing it in your Application.cfc), it will always return true, allowing the request to continue. If you implement it, you can check for things like an API key, or whether or not the customer has paid for your service, and return something other than the data that they are requesting.
If you do not return TRUE, allowing the request to continue as normal, then Taffy expects you to return a **[representation](/atuttle/Taffy/wiki/Using-a-Custom-Representation-Class)** (either the default class, or a custom one) that it should immediately return to the consumer, serialized to the appropriate format. If you simply want to return with a status code of 403 (which indicates "Not Allowed"), you could do this:
```js
return newRepresentation().noData().withStatus(403);
```
Alternately, you could return some data to indicate that they owe you money or something:
```js
return newRepresentation()
.setData({error="Your account is past due. Please email accounts payable."})
.withStatus(403);
```
The options here are limited only by your imagination.
You can add data to the **requestArguments** structure and this will be passed on to any resource that handles the request. Simply add a key to the structure:
```js
function onTaffyRequest(verb, cfc, requestArguments, mimeExt, headers) {
arguments.requestArguments.myData = "myvalue";
return true;
}
```
In your resource:
```js
function get(myData) {
//arguments.myData = myValue
}
```
### Resources
Resource CFCs extend `taffy.core.resource`. The following methods are available inside each of your Resource CFCs:
#### noData()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:** _(none)_
This method allows you to specify that there is no data to be returned for the current request. Generally, you would use it in conjunction with the **withStatus** method to set a specific return status for the request. For example, if the requested resource doesn't exist, you could return a 404 error like so:
```js
return noData().withStatus(404);
```
#### queryToArray()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **data (query)** - The query object to be transformed
This method transforms a ColdFusion query object into an array of structures. It was added because ColdFusion's serializeJSON functionality uses an ..._eccentric_... format for queries. **queryToArray** returns the format most people expect: a vanilla array of structures with named keys. To be fair the ACF serialization format uses less data as long as there is more than 1 row in the query, but it doesn't matter that you do a better job if nobody understands your output. _queryToArray also preserves query column name case, which serializeJSON does not._
```js
return representationOf(queryToArray(someQuery));
```
#### representationOf()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **data (any)** - The data to return to the consumer.
Data can be of any type, including complex data types like queries, structures, and arrays, as long as the serializer knows how to serialize them. For more information on using a custom representation class, see [Using a custom Representation Class](https://github.com/atuttle/Taffy/wiki/Using-a-Custom-Representation-Class).
#### saveLog()
**Use it inside:** anywhere inside a Resource CFC to log data using your [configured logging adapter](https://github.com/atuttle/Taffy/wiki/Exception-Logging-Adapters).<br/>
**Parameters:**
- **exception (struct)** - traditionally a CF exception object, but any struct may be passed.
What you pass to this method is simply handed off to the logging adapter. You may use one of the included adapters (LogToEmail, LogToBuglogHQ, or LogToHoth), or a custom logging adapter. If you write a custom logging adapter, it should implement the `taffy.bonus.ILogAdapter` interface.
If you don't configure a logging adapter, the default is LogToEmail, but the default `from` and `to` email addresses are not useful. See [Exception Log Adapters](https://github.com/atuttle/Taffy/wiki/Exception-Logging-Adapters) for more information on configuring logging adapters.
#### streamBinary()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **binaryData (any)** - the binary data to be streamed back to the consumer
Use this method in place of `representationOf()` to return a stream of binary data. Useful for streaming things like dynamically generated PDFs.
#### streamFile()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **fileName (string)** - fully qualified file path (eg c:\tmp\files.zip)
Use this method in place of `representationOf()` to stream a file from disk (or VFS).
#### streamImage()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **binaryData (any)** - the binary, base64 encoded data of the image, to be streamed back to the consumer
Use this method in place of `representationOf()` to stream an image from disk (or VFS).
#### withHeaders()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **headerStruct (struct)** - A structure whose keys are desired header names ("x-powered-by") and whose associated values are the values for the corresponding headers ("Taffy 1.1!").
This special method _**requires**_ the use of either **noData** or **representationOf**. It adds custom headers to the return. Additional use of **withStatus** optional.
```js
return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"});
```
#### withMime()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **mime (string)** - mime type (eg. "application/pdf") to be returned with the streamed file data
This special method _**requires**_ the use of either **streamFile** or **streamBinary**. It overrides the default mime type header for the return.
```js
return streamFile("kittens/cuteness.pdf").withMime("application/pdf");
```
#### withStatus()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **statusCode (numeric)** - the [HTTP Status Code](http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html) to return to the consumer.
- **statusText (string)** - the Status Text to include with the HTTP Status Code. The "OK" part of "200 OK"; the "Not Found" part of "404 Not Found".
This special method _**requires**_ the use of either **noData** or **representationOf**. It sets the HTTP Status Code of the return. Additional use of **withHeaders** optional.
_If you do not specify a return status code, Taffy will always return status code 200 (OK) by default._
```js
return noData().withStatus(404, "Not Found");
```
### Custom Representation Classes
Representation Classes have the job of converting the native data that your resources return (queries, structures, arrays, and so on...) into &mdash;usually&mdash; a string that can be sent to the client and used there. It's also possible to send images and binary files like PDFs and ZIPs, but that's a topic that has its own guide. Taffy comes with a representation class that uses ColdFusion's **serializeJson** method to convert native data to JSON strings, and uses this class by default.
_Why would you want to create a custom representation class, and how would you do it?_
#### Why
You want to create a Custom Representation Class ("CRC") if you're stymied by [the bugs in ColdFusion's serializeJson method](http://fusiongrokker.com/post/you-can-help-fix-coldfusion-json-serialization), or if you want to return something other than JSON: XML for example. Another reason to use a CRC is when your api should be capable of returning more than one return format &mdash; e.g. both XML and JSON.
#### How
A Custom Representation Class is a CFC that:
- Extends `taffy.core.baseRepresentation`
- Implements 1 or more "getAs{something}" methods
- ... that defines a `taffy:mime` attribute
Let's take a look at **taffy.core.nativeJsonRepresentation** &mdash; the default representation class that Taffy uses unless you configure something else.
```xml
<cfcomponent extends="taffy.core.baseRepresentation" output="false">
<cffunction
name="getAsJson"
output="false"
taffy:mime="application/json"
taffy:default="true"
hint="serializes data as JSON">
<cfreturn serializeJSON(variables.data) />
</cffunction>
</cfcomponent>
```
- Extends `taffy.core.baseRepresentation`? Check.
- Implements 1 or more "getAs{something}" methods? Check.
- ... that defines a `taffy:mime` attribute? Check.
The base class is easy to explain: It makes everything about CRC's work without you having to do anything. It's the black magic.
What's the `getAs{something}` method for? Well, it's for **get**ting the data out of your CRC in a format that the user is requesting. The client can specify its desired return format in one of two ways. An HTTP `Accept` header, or by appending `.{format}` to the URL.
The former looks like: `Accept: application/json`, and the latter looks like:
```
http://example.com/api/v1/index.cfm/users.json
```
You may be familiar or comfortable with either, but the point is that they both mean the same thing. Taffy looks at the methods in your CRC at startup to determine what types your API will support. It needs both the format-extension (.json, via `getAsJson`) and the mime type (application/json, via taffy:mime) in order to function properly.
When writing your own CRC, you can assume that a resource has returned data and the returned value is available to the CRC as `variables.data`. This is why the default class returns `serializeJson(variables.data)`. If your method is `getAsXML` then it needs to be able to convert the data into an XML string.
#### Multiple Formats
Since a common use-case for CRCs is supporting multiple data formats, we'll next look at **CustomRepresentationClass.cfc**, found in Taffy's **examples/api_twoFormats/resources** folder.
```xml
<cfcomponent extends="taffy.core.baseRepresentation">
<cfset variables.jsonUtil = application.jsonUtil />
<cfset variables.AnythingToXML = application.AnythingToXML />
<cffunction name="getAsJSON" taffy:mime="application/json" taffy:default="true" output="false">
<cfreturn variables.jsonUtil.serializeJson(variables.data) />
</cffunction>
<cffunction name="getAsXML" taffy:mime="application/xml" output="false">
<cfreturn variables.AnythingToXML.ToXML(variables.data) />
</cffunction>
</cfcomponent>
```
Here we see two getters: `getAsJSON` with `taffy:mime="application/json"` and `getAsXML` with `taffy:mime="application/xml"`. Each is using a 3rd party library (available in the example folder) to do the serialization work. It's assuming that these libraries are available in Application scope (check Application.cfc for the implementation). This is generally regarded as a bad practice ("tight coupling"), and Taffy does not require that your code be structured this way. It's only included this way to keep the example as simple and focused as possible.
When a request is made that wants XML back, Taffy calls the `getAsXML` method to get the XML string. When a request is made for JSON data, Taffy calls `getAsJSON`.
And that &mdash;in a nutshell&mdash; is Custom Representation Classes.
+717
View File
@@ -0,0 +1,717 @@
# Taffy
The REST Web Service framework for ColdFusion and Railo
<a id="download" href="https://github.com/atuttle/Taffy/archive/v2.0.1.zip">Download Taffy 2.0.1.zip</a>
- [Release Notes](https://github.com/atuttle/Taffy/releases/tag/v2.0.1)
- [GitHub](https://github.com/atuttle/Taffy)
- Join us in the **#taffy** chat room on the [CFML Slack](https://cfml-slack.herokuapp.com/)
## Quickstart: Your First API
**Application.cfc:**
```js
component extends="taffy.core.api" {}
```
**index.cfm:**
```html
<!-- this space left intentionally blank -->
```
**/resources/hello.cfc:**
```js
component extends="taffy.core.resource" taffy_uri="/hello" {
function get(){
return representationOf(['hello','world']);
}
}
```
Congratulations, you've just written your first API. Didn't think it could be so simple, did you? Indeed, [you can fit an entire Taffy API into a single tweet](https://twitter.com/cf_taffy/statuses/327415972581486592).
Point your browser to the empty **index.cfm** file you created, and have a look at the dashboard. Click the "hello" row to expand it, and click the **Send** button to make a REST request. The results of your request are displayed below.
The response includes a **status code** and **status text**, as well as other **headers**, and the **response body** (if applicable). Taffy also shows you the amount of time the request took.
### Getting Started, with more details
Here's a [Getting Started guide](https://github.com/atuttle/Taffy/wiki/Getting-Started) that goes into a little bit more detail, if the above example wasn't enough.
## Installation Options
### Global /taffy Mapping
Best practice would be to keep Taffy _out of your web root_. This is also (almost) the easiest install method. Instead of putting taffy in the web root, use a global mapping (set in the CF Administrator) pointing `/taffy` to wherever you have it saved.
### /taffy in Web Root
Just put the /taffy folder at the root of your domain. Boom, done.
### Sub-folder
If you aren't allowed to create a global mapping, or for some other reason want to, you can install Taffy as a sub-folder of your API. (NB: This would also allow you to use multiple versions of Taffy on the same CF Instance.)
Using sub-folders requires the use of Application-Specific Mappings (introduced in Adobe ColdFusion 8). Start by creating this directory structure:
```
/path-to-your-api
├── Application.cfc
├── index.cfm
├─┬ /taffy
│ ├── /bonus
│ ├── /core
│ └── /dashboard
└─┬ /resources
├── ...
└── someResource.cfc
```
You can see that the taffy folder is a sibling to Application.cfc. This allows Application.cfc to use relative paths to extend `taffy.core.api`.
Next, if your Application.cfc and `/resources/` folder aren't in the web-root (e.g. they're inside something like `/api/`) then you'll need to add an [application-specific mapping](http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=appFramework_04.html) for `/resources` so that Taffy can find your resources to initialize the routes.
```js
this.mappings["/resources"] = expandPath("./resources");
```
You'll also need to add a mapping for `/taffy` so that the resources can extend `taffy.core.resource` (since the taffy folder isn't a child of the resources folder):
```js
this.mappings["/taffy"] = expandPath("./taffy");
```
## Authentication and Security
If your authentication and security requirements are simple, you may find the combination of SSL and HTTP Basic Auth to be sufficient. I would advise you not to use Basic Auth without SSL, as it is then easily sniffed. If you're utilizing Basic Auth, Taffy 1.3 added [getBasicAuthCredentials()](#getbasicauthcredentials).
If you're after something a bit more complex, but still short of OAuth, I've written an article covering [Advanced Authentication with Taffy](https://adamtuttle.codes/blog/2013/advanced-authentication-with-taffy-apis/).
Implementing OAuth is something I would like to document, but it is a fairly large topic and I haven't had the time yet. Pull requests welcome.
## Railo Idiosyncrasies
### Custom HTTP Status Messages
Taffy allows for setting the HTTP status message using [.withStatus()](#withstatus), such as:
```js
return representationOf({...}).withStatus(403, "Not Authorized");
```
Railo's default setup on Tomcat doesn't allow changing the Status Text header value; so you might use `.withStatus(403, "Account Past Due")` but Tomcat changes this back to `403 Not Authorized`.
You can either work around this by passing status information in the response body, or alternately changing **catalina.properties** (Tomcat config) to include the line:
```conf
org.apache.coyote.USE_CUSTOM_STATUS_MSG_IN_HEADER=true
```
[Hat tip to Jean-Bernard van Zuylen](https://github.com/atuttle/Taffy/issues/121#issuecomment-23410624)
### PUT requests & FORM scope
Prior to versions 4.1.1.002 and 4.2 of Railo, a PUT request would populate the FORM scope. This probably doesn't cause any issues with API's, but did cause a pair of unit tests to fail. (put_body_is_mime_content, and put_body_is_url_encoded_params) This is safe to ignore. Also, upgrade, man!
[Hat tip to Jean-Bernard van Zuylen](https://github.com/atuttle/Taffy/issues/121#issuecomment-23410624)
## More Guides
Some guides are too broad for this document. For your benefit, they are linked here:
- [Migration Guide](https://github.com/atuttle/Taffy/wiki/Migration-Guide) - Notes on upgrading from 1.3.x to 2.0.x
## Configuration Reference
### Configuration via Metadata
Metadata is used to apply configuration in a concise and elegant manor, where necessity and possibility intersect.
#### In Resources
Resources are CFCs that interpret a request and provide or update the requested data.
##### taffy:uri
The **taffy:uri** property applies to the `<cfcomponent>` tag or the `component{}` definition. It configures which URIs the CFC will respond to. There is no limit to the number of tokens that can be used in a single URI, as long as they are not consecutive and are delimited by at least one character (like a slash). Examples:
```xml
<cfcomponent taffy:uri="/artist/{artistId}">
</cfcomponent>
```
or
```js
component taffy_uri="/artist/{artistId}" {
}
```
The following example is _invalid_, because the tokens are not separated by (at least) a slash:<br/>
```
taffy:uri="/artist/{artistId}{artistName}"
```
##### Tokens
Tokens in URIs define the parts of the URI that are dynamic. They are identified by curly-brackets: `{tokenName}` and are passed **by name** to all functions that handle the request. The `{foo}` token's value will be passed to the `foo` argument of the associated method.
##### URI Matching Order
As of Taffy 1.3, URIs are processed into a sorted array on API startup, and searched in order for every request, such that `/artists/list` will match before `/artists/{id}`. You should design your API URI's accordingly. Pay special attention to placement and possible values for URI tokens. If a possible value is the same as a static URI, consider changing URI formatting. For example, if we had an artist with id "list", then having the static URI `/artists/list` would prevent the `/artists/{id}` URI from ever matching when the intention is to get the individual artist record for artist with id "list."
##### taffy:verb
By convention, resources will automatically map the 4 primary HTTP REST verbs -- GET, PUT, POST, DELETE -- to CFC methods with the same name. For extended verbs -- OPTIONS, HEAD -- or if you want to map one of the primary verbs to a method with a different name, you can use the `taffy:verb` metadata property on the method to specify the verb it should respond to. Examples:
```xml
<cffunction name="getUser" taffy:verb="get">
<cfargument name="userId" type="numeric" />
</cffunction>
```
or
```js
function getUser( numeric userId ) taffy_verb="get" {
}
```
#### In Representation Classes
Representation classes are used to take the data provided by a resource and serialize it into a format usable by the web service consumer. A single Representation Class is capable of serializing native data objects (strings, numbers, queries, structures, arrays, etc) into 1 or more formats. Typical formats include JSON, XML, or YAML, but are not limited.
##### taffy:mime
By convention, the mime-types supported by your API are determined by the method names in your default representation class. (The included default representation class supports only JSON.) Each `getAsX` method in your representation class describes a new mime type, defined in two parts: The X portion of the method name determines the extension of the mime type -- which can be appended to the URI as if it were a file, as in: `/artists/42.json` which would use `getAsJson`. Taffy will also return a content-type header with the content type that you supply in the `taffy:mime` metadata property of the `getAsX` method. Typically, this is something like "application/json" or "application/xml". Examples:
```xml
<cffunction name="getAsJson" taffy:mime="application/json">
</cffunction>
```
or
```js
function getAsJson() taffy_mime="application/json" {
}
```
##### taffy:default
When your API supports more than one data format (i.e. json and xml), you must set one as the default. You do this with the `taffy:default` property, which expects a boolean value of either TRUE or FALSE. The default is FALSE, and you never need to include `taffy:default="false"`. You only need to include `taffy:default="true"` on one method -- the one that should be the default. Examples:
```xml
<cffunction name="getAsJson" taffy:mime="application/json" taffy:default="true">
</cffunction>
<cffunction name="getAsXml" taffy:mime="application/xml">
</cffunction>
```
or
```js
function getAsJson() taffy_mime="application/json" taffy_default="true" {}
function getAsXml() taffy_mime="application/xml" {}
```
### variables.framework settings
Default values:
```js
variables.framework = {
reloadKey = "reload",
reloadPassword = "true",
reloadOnEveryRequest = false,
endpointURLParam = "endpoint",
representationClass = "taffy.core.nativeJsonRepresentation",
dashboardKey = "dashboard",
disableDashboard = false,
disabledDashboardRedirect = "",
jsonp = false,
unhandledPaths = "/flex2gateway",
allowCrossDomain = false,
globalHeaders = structNew(),
debugKey = "debug",
useEtags = false,
returnExceptionsAsJson = true,
exceptionLogAdapter = "taffy.bonus.LogToEmail",
exceptionLogAdapterConfig = {
emailFrom = "api-error@yourdomain.com",
emailTo = "you@yourdomain.com",
emailSubj = "Exception Trapped in API",
emailType = "html"
},
beanFactory = "",
environments = {}
};
```
#### reloadKey
**Available in:** Taffy 1.2+<br/>
**Type:** String<br/>
**Default:** "reload"<br/>
**Description:** Name of the url parameter that requests the framework to be reloaded. Used in combination with the reload password (see: reloadPassword), the framework will re-initialize itself. During re-initialization, all configuration settings are re-applied and all cached objects are cleared and reloaded. If the value of the key does not match the reload password, a reload will not be performed. This allows you to set a secret password to restrict control of reloading your API to trusted parties.
#### reloadPassword
**Available in:** Taffy 1.2+<br/>
**Type:** String<br/>
**Default:** "true"<br/>
**Description:** Accepted value of the url parameter that requests the framework to be reloaded. Used in combination with the reload key (see: reloadKey), the framework will re-initialize itself. During re-initialization, all configuration settings are re-applied and all cached objects are cleared and reloaded. If the value of the key does not match the reload password, a reload will not be performed. This allows you to set a secret password to restrict control of reloading your API to trusted parties.
#### reloadOnEveryRequest
**Available in:** Taffy 1.2+<br/>
**Type:** Boolean<br/>
**Default:** False<br/>
**Description:** Flag that indicates whether Taffy should reload cached values and configuration on every request. Useful in development; set to FALSE in production.
#### endpointURLParam
**Available in:** Taffy 1.3+<br/>
**Type:** String<br/>
**Default:** "endpoint"<br/>
**Description:** The query-string parameter name that can optionally be used to specify URI. Until now, URI formatting has been required to be index.cfm/URI; this parameter allows you to use index.cfm?endpoint=/URI. This setting (endpointURLParam) allows you to change the default parameter name of "endpoint" to something custom.
#### representationClass
**Available in:** Taffy 1.2+<br/>
**Type:** String<br/>
**Default:** "taffy.core.nativeJsonRepresentation"<br/>
**Description:** The CFC dot-notation path, or bean name, of the representation class that your API will use to serialize returned data for the client.
#### dashboardKey
**Available in:** Taffy 1.2+<br/>
**Deprecated **in: Taffy 1.3+<br/>
**Type:** String<br/>
**Default:** "dashboard"
Description: Name of the url parameter that displays the dashboard. The dashboard displays resources that your API is aware of, generates documentation about your API based on hint attributes, and contains a mock client to make testing your API easy.
#### disableDashboard
**Available in:** Taffy 1.2+<br/>
**Type:** Boolean<br/>
**Default:** False<br/>
**Description:** Whether or not Taffy will allow the dashboard to be displayed. If set to true, the dashboard key is simply ignored. You may wish to disable the dashboard in production, depending on whether or not you want customers/clients to be able to see it.
#### disabledDashboardRedirect
**Available in:** Taffy 1.3+<br/>
**Type:** String<br/>
**Default:** ""<br/>
**Description:** URL to which Taffy should redirect (302) the client/browser if the dashboard is disabled. If the dashboard is disabled and this value is blank, a simple 403 Forbidden response is sent instead.
#### jsonp
**Available in:** Taffy 2.0+<br/>
**Type:** Boolean/String<br/>
**Default:** false<br/>
**Description:** When false, JSONP is disabled. To enable it, change the value to a string, such as "callback". The value you specify will be the query parameter in which Taffy expects to find the JSONP callback name. Note: JSONP only works for GET requests (by design!)
#### unhandledPaths
**Available in:** Taffy 1.2+<br/>
**Type:** String (Comma-delimited list)<br/>
**Default:** "/flex2gateway"<br/>
**Description:** Set a list of paths (usually subfolders of the API) that you do not want Taffy to interfere with. Unless listed here, Taffy takes over the request lifecycle and does not execute the requested ColdFusion template.
#### allowCrossDomain
**Available in:** Taffy 1.2+<br/>
**Type:** Boolean<br/>
**Default:** False<br/>
**Description:** Whether or not to allow cross-domain access to your API. Turning this on adds the following headers:
```xml
<cfheader name="Access-Control-Allow-Origin" value="*" />
<cfheader name="Access-Control-Allow-Methods" value="#allowedVerbs#" />
<cfheader name="Access-Control-Allow-Headers" value="Content-Type" />
```
The allowed verbs, of course, are the ones allowed by the requested resource, as well as OPTIONS.
#### globalHeaders
**Available in:** Taffy 1.2+<br/>
**Type:** Structure<br/>
**Default:** {}<br/>
**Description:** A structure where each key is the name of a header you want to return, such as "X-MY-HEADER" and the structure value is the header value.
Global headers are static. You set them on application initialization and they do not change. If you need dynamic headers, you can add them to each response at runtime using withHeaders().
#### debugKey
**Available in:** Taffy 1.2+<br/>
**Type:** String<br/>
**Default:** "debug"<br/>
**Description:** Name of the url parameter that enables CF Debug Output.
#### useEtags
**Available in:** Taffy 1.3+<br/>
**Type:** Boolean<br/>
**Default:** False<br/>
**Description:** Enable the use of HTTP ETags for caching purposes. Taffy will automatically handle both sending the server ETag value and detecting client supplied ETags (via the If-None-Match header) for you; simply turn this setting on.
**NOTE FOR RAILO USERS:** While it will not cause errors, the underlying Java code used in this feature was improperly implemented prior to Railo 4.0.? and this could result in your result data being sent as if it were changed when it in fact has not. (I'm not sure which Railo point release will include the fix. The latest as of this writing is version 4.0.2, and does not include it.) Adobe ColdFusion is unaffected.
#### returnExceptionsAsJson
**Available in:** Taffy 1.2+<br/>
**Type:** Boolean<br/>
**Default:** true<br/>
**Description:** When an error occurs that is not otherwise handled, this option tells Taffy to attempt to format the error information as JSON and return that (regardless of the requested return format).
#### exceptionLogAdapter
**Available in:** Taffy 1.2+<br/>
**Type:** String<br/>
**Default:** "taffy.bonus.LogToEmail"<br/>
**Description:** CFC dot-notation path to the exception logging adapter you want to use. Default adapter simply emails all exceptions. See Exception Logging Adapters for more details.
#### exceptionLogAdapterConfig
**Available in:** Taffy 1.2+<br/>
**Type:** Any<br/>
**Default:** See defaults [here](#variablesframework-settings)<br/>
**Description:** Configuration that your chosen logging adapter requires. Can be any data type. See Exception Logging Adapters for more details.
#### beanFactory
**Available in:** Taffy 1.2+<br/>
**Type:** Object Instance<br/>
**Default:** ""<br/>
**Description:** Already instantiated and cached (e.g. in Application scope) object instance of your external bean factory. Not required in order to use Taffy's built-in factory.
#### environments
**Available in:** Taffy 1.3+<br/>
**Type:** Structure<br/>
**Default:** {}<br/>
**Description:** Environment-specific overrides to any framework settings. Applied after general variables.framework settings, and after configureTaffy() has been called. See [getEnvironment()](#getenvironment) for more details.
## Index of API Methods
### Application.cfc
#### getPath()
**Parameters:** _(none)_
This method is provided as an extension point. On Adobe ColdFusion ("ACF") 9, installed in standard entire-server mode, no change should be necessary. However, if ACF is installed on another JEE app server (i.e. Tomcat, Glassfish, etc), or on JRun but using an EAR/WAR setup, then you may need to override this method to make Taffy work on your server. See [[GetPath Setups]] for more information.
#### getBasicAuthCredentials()
**Parameters:** _(none)_
**Added in Taffy 1.3.** This method returns a structure with two keys: `username`, and `password`. When the client does not provide HTTP Basic Auth credentials, the username and password keys will be blank. When the client does provide them, the values will be available in these keys.
This method is only available inside your Application.cfc. If, for example, you need access to the username in a resource, you can use this method inside onTaffyRequest and add the username to the requestArguments structure.
#### getBeanFactory()
**Parameters:** _(none)_
Returns whatever bean factory you may have set into Taffy, if any. If using the `/resources` folder, it returns Taffy's built-in factory.
#### newRepresentation()
**Use it inside:** onTaffyRequest<br/>
**Parameters:**
- **class (string)** - the dot-notation cfc path (or bean id if managing the class with a bean factory, including the built-in factory) for the representation class. Optional. Default value is the defined representation class. If you're using the framework default representation class, it's `nativeJsonRepresentation`.
Use this method inside onTaffyRequest when you want to _abort_ the request with some specific message or data. Use the `setData` method on the returned representation class to put your data/message into it before returning it.
#### getEnvironment()
**Parameters:** _(none)_
Taffy calls this method during initialization to determine in which configured environment, if any, it is executing. Overriding it is optional. Use whatever methodology you like to determine the result (e.g. hostname, reading a file or registry setting, etc.), and simply return, as string, the name of the current environment.
The returned value will be used to load environment-specific configuration. For example, if you have the following code in your Application.cfc, then the dashboard will be disabled in production:
```js
variables.framework = {
disableDashboard = false
,environments = {
production = {
disableDashboard = true
}
}
};
function getEnvironment(){
if (...){
return "production";
}else{
return "development";
}
}
```
#### onTaffyRequest()
**Parameters:**
- **verb (string)** - The HTTP request verb provided by the client
- **cfc (string)** - The CFC name (minus ".cfc") that would handle the request. (Bean Name, if using an external bean factory.)
- **requestArguments (struct)** - A structure containing all of the arguments of the request, including tokens from the URI as well as any query string parameters.
- **mimeExt (string)** - The mime extension (e.g. "json" - NOT the full mime type, e.g. "application/json")
- **headers (struct)** - A structure containing each header from the request, as sent by the client.
This method is optional, and allows you to inspect and potentially abort an API request in a way that adheres to the HTTP specification. If you choose not to override it (by implementing it in your Application.cfc), it will always return true, allowing the request to continue. If you implement it, you can check for things like an API key, or whether or not the customer has paid for your service, and return something other than the data that they are requesting.
If you do not return TRUE, allowing the request to continue as normal, then Taffy expects you to return a **[representation](/atuttle/Taffy/wiki/Using-a-Custom-Representation-Class)** (either the default class, or a custom one) that it should immediately return to the consumer, serialized to the appropriate format. If you simply want to return with a status code of 403 (which indicates "Not Allowed"), you could do this:
```js
return newRepresentation().noData().withStatus(403);
```
Alternately, you could return some data to indicate that they owe you money or something:
```js
return newRepresentation()
.setData({error="Your account is past due. Please email accounts payable."})
.withStatus(403);
```
The options here are limited only by your imagination.
You can add data to the **requestArguments** structure and this will be passed on to any resource that handles the request. Simply add a key to the structure:
```js
function onTaffyRequest(verb, cfc, requestArguments, mimeExt, headers) {
arguments.requestArguments.myData = "myvalue";
return true;
}
```
In your resource:
```js
function get(myData) {
//arguments.myData = myValue
}
```
### Resources
Resource CFCs extend `taffy.core.resource`. The following methods are available inside each of your Resource CFCs:
#### noData()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:** _(none)_
This method allows you to specify that there is no data to be returned for the current request. Generally, you would use it in conjunction with the **withStatus** method to set a specific return status for the request. For example, if the requested resource doesn't exist, you could return a 404 error like so:
```js
return noData().withStatus(404);
```
#### queryToArray()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **data (query)** - The query object to be transformed
This method transforms a ColdFusion query object into an array of structures. It was added because ColdFusion's serializeJSON functionality uses an ..._eccentric_... format for queries. **queryToArray** returns the format most people expect: a vanilla array of structures with named keys. To be fair the ACF serialization format uses less data as long as there is more than 1 row in the query, but it doesn't matter that you do a better job if nobody understands your output. _queryToArray also preserves query column name case, which serializeJSON does not._
```js
return representationOf(queryToArray(someQuery));
```
#### representationOf()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **data (any)** - The data to return to the consumer.
Data can be of any type, including complex data types like queries, structures, and arrays, as long as the serializer knows how to serialize them. For more information on using a custom representation class, see [Using a custom Representation Class](https://github.com/atuttle/Taffy/wiki/Using-a-Custom-Representation-Class).
#### saveLog()
**Use it inside:** anywhere inside a Resource CFC to log data using your [configured logging adapter](https://github.com/atuttle/Taffy/wiki/Exception-Logging-Adapters).<br/>
**Parameters:**
- **exception (struct)** - traditionally a CF exception object, but any struct may be passed.
What you pass to this method is simply handed off to the logging adapter. You may use one of the included adapters (LogToEmail, LogToBuglogHQ, or LogToHoth), or a custom logging adapter. If you write a custom logging adapter, it should implement the `taffy.bonus.ILogAdapter` interface.
If you don't configure a logging adapter, the default is LogToEmail, but the default `from` and `to` email addresses are not useful. See [Exception Log Adapters](https://github.com/atuttle/Taffy/wiki/Exception-Logging-Adapters) for more information on configuring logging adapters.
#### streamBinary()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **binaryData (any)** - the binary data to be streamed back to the consumer
Use this method in place of `representationOf()` to return a stream of binary data. Useful for streaming things like dynamically generated PDFs.
#### streamFile()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **fileName (string)** - fully qualified file path (eg c:\tmp\files.zip)
Use this method in place of `representationOf()` to stream a file from disk (or VFS).
#### streamImage()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **binaryData (any)** - the binary, base64 encoded data of the image, to be streamed back to the consumer
Use this method in place of `representationOf()` to stream an image from disk (or VFS).
#### withHeaders()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **headerStruct (struct)** - A structure whose keys are desired header names ("x-powered-by") and whose associated values are the values for the corresponding headers ("Taffy 1.1!").
This special method _**requires**_ the use of either **noData** or **representationOf**. It adds custom headers to the return. Additional use of **withStatus** optional.
```js
return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"});
```
#### withMime()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **mime (string)** - mime type (eg. "application/pdf") to be returned with the streamed file data
This special method _**requires**_ the use of either **streamFile** or **streamBinary**. It overrides the default mime type header for the return.
```js
return streamFile("kittens/cuteness.pdf").withMime("application/pdf");
```
#### withStatus()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **statusCode (numeric)** - the [HTTP Status Code](http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html) to return to the consumer.
- **statusText (string)** - the Status Text to include with the HTTP Status Code. The "OK" part of "200 OK"; the "Not Found" part of "404 Not Found".
This special method _**requires**_ the use of either **noData** or **representationOf**. It sets the HTTP Status Code of the return. Additional use of **withHeaders** optional.
_If you do not specify a return status code, Taffy will always return status code 200 (OK) by default._
```js
return noData().withStatus(404, "Not Found");
```
### Custom Representation Classes
Representation Classes have the job of converting the native data that your resources return (queries, structures, arrays, and so on...) into &mdash;usually&mdash; a string that can be sent to the client and used there. It's also possible to send images and binary files like PDFs and ZIPs, but that's a topic that has its own guide. Taffy comes with a representation class that uses ColdFusion's **serializeJson** method to convert native data to JSON strings, and uses this class by default.
_Why would you want to create a custom representation class, and how would you do it?_
#### Why
You want to create a Custom Representation Class ("CRC") if you're stymied by [the bugs in ColdFusion's serializeJson method](http://fusiongrokker.com/post/you-can-help-fix-coldfusion-json-serialization), or if you want to return something other than JSON: XML for example. Another reason to use a CRC is when your api should be capable of returning more than one return format &mdash; e.g. both XML and JSON.
#### How
A Custom Representation Class is a CFC that:
- Extends `taffy.core.baseRepresentation`
- Implements 1 or more "getAs{something}" methods
- ... that defines a `taffy:mime` attribute
Let's take a look at **taffy.core.nativeJsonRepresentation** &mdash; the default representation class that Taffy uses unless you configure something else.
```xml
<cfcomponent extends="taffy.core.baseRepresentation" output="false">
<cffunction
name="getAsJson"
output="false"
taffy:mime="application/json"
taffy:default="true"
hint="serializes data as JSON">
<cfreturn serializeJSON(variables.data) />
</cffunction>
</cfcomponent>
```
- Extends `taffy.core.baseRepresentation`? Check.
- Implements 1 or more "getAs{something}" methods? Check.
- ... that defines a `taffy:mime` attribute? Check.
The base class is easy to explain: It makes everything about CRC's work without you having to do anything. It's the black magic.
What's the `getAs{something}` method for? Well, it's for **get**ting the data out of your CRC in a format that the user is requesting. The client can specify its desired return format in one of two ways. An HTTP `Accept` header, or by appending `.{format}` to the URL.
The former looks like: `Accept: application/json`, and the latter looks like:
```
http://example.com/api/v1/index.cfm/users.json
```
You may be familiar or comfortable with either, but the point is that they both mean the same thing. Taffy looks at the methods in your CRC at startup to determine what types your API will support. It needs both the format-extension (.json, via `getAsJson`) and the mime type (application/json, via taffy:mime) in order to function properly.
When writing your own CRC, you can assume that a resource has returned data and the returned value is available to the CRC as `variables.data`. This is why the default class returns `serializeJson(variables.data)`. If your method is `getAsXML` then it needs to be able to convert the data into an XML string.
#### Multiple Formats
Since a common use-case for CRCs is supporting multiple data formats, we'll next look at **CustomRepresentationClass.cfc**, found in Taffy's **examples/api_twoFormats/resources** folder.
```xml
<cfcomponent extends="taffy.core.baseRepresentation">
<cfset variables.jsonUtil = application.jsonUtil />
<cfset variables.AnythingToXML = application.AnythingToXML />
<cffunction name="getAsJSON" taffy:mime="application/json" taffy:default="true" output="false">
<cfreturn variables.jsonUtil.serializeJson(variables.data) />
</cffunction>
<cffunction name="getAsXML" taffy:mime="application/xml" output="false">
<cfreturn variables.AnythingToXML.ToXML(variables.data) />
</cffunction>
</cfcomponent>
```
Here we see two getters: `getAsJSON` with `taffy:mime="application/json"` and `getAsXML` with `taffy:mime="application/xml"`. Each is using a 3rd party library (available in the example folder) to do the serialization work. It's assuming that these libraries are available in Application scope (check Application.cfc for the implementation). This is generally regarded as a bad practice ("tight coupling"), and Taffy does not require that your code be structured this way. It's only included this way to keep the example as simple and focused as possible.
When a request is made that wants XML back, Taffy calls the `getAsXML` method to get the XML string. When a request is made for JSON data, Taffy calls `getAsJSON`.
And that &mdash;in a nutshell&mdash; is Custom Representation Classes.
+797
View File
@@ -0,0 +1,797 @@
# Taffy
The REST Web Service framework for ColdFusion and Railo
<a id="download" href="https://github.com/atuttle/Taffy/archive/v2.1.0.zip">Download Taffy 2.1.0.zip</a>
- [Release Notes](https://github.com/atuttle/Taffy/releases/tag/v2.1.0)
- [GitHub](https://github.com/atuttle/Taffy)
- Join us in the **#taffy** chat room on the [CFML Slack](https://cfml-slack.herokuapp.com/)
## Quickstart: Your First API
**Application.cfc:**
```js
component extends="taffy.core.api" {}
```
**index.cfm:**
```html
<!-- this space left intentionally blank -->
```
**/resources/hello.cfc:**
```js
component extends="taffy.core.resource" taffy_uri="/hello" {
function get(){
return representationOf(['hello','world']);
}
}
```
Congratulations, you've just written your first API. Didn't think it could be so simple, did you? Indeed, [you can fit an entire Taffy API into a single tweet](https://twitter.com/cf_taffy/statuses/327415972581486592).
Point your browser to the empty **index.cfm** file you created, and have a look at the dashboard. Click the "hello" row to expand it, and click the **Send** button to make a REST request. The results of your request appear below the request parameters.
The response includes a **status code** and **status text**, as well as other **headers**, and the **response body** (if applicable). Taffy also shows you the amount of time the request took.
### Getting Started, with more details
Here's a [Getting Started guide](https://github.com/atuttle/Taffy/wiki/Getting-Started) that goes into a little bit more detail, if the above example wasn't enough.
## Installation Options
### Global /taffy Mapping
Best practice would be to keep Taffy _out of your web root_. This is also (almost) the easiest install method. Instead of putting taffy in the web root, use a global mapping (set in the CF Administrator) pointing `/taffy` to wherever you have it saved.
### /taffy in Web Root
Just put the /taffy folder at the root of your domain. Boom, done.
### Sub-folder
If you aren't allowed to create a global mapping, or for some other reason want to, you can install Taffy as a sub-folder of your API. (NB: This would also allow you to use multiple versions of Taffy on the same CF Instance.)
Using sub-folders requires the use of Application-Specific Mappings (introduced in Adobe ColdFusion 8). Start by creating this directory structure:
```
/path-to-your-api
├── Application.cfc
├── index.cfm
├─┬ /taffy
│ ├── /bonus
│ ├── /core
│ └── /dashboard
└─┬ /resources
├── ...
└── someResource.cfc
```
You can see that the taffy folder is a sibling to Application.cfc. This allows Application.cfc to use relative paths to extend `taffy.core.api`.
Next, if your Application.cfc and `/resources/` folder aren't in the web-root (e.g. they're inside something like `/api/`) then you'll need to add an [application-specific mapping](http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=appFramework_04.html) for `/resources` so that Taffy can find your resources to initialize the routes.
```js
this.mappings["/resources"] = expandPath("./resources");
```
You'll also need to add a mapping for `/taffy` so that the resources can extend `taffy.core.resource` (since the taffy folder isn't a child of the resources folder):
```js
this.mappings["/taffy"] = expandPath("./taffy");
```
## Authentication and Security
If your authentication and security requirements are simple, you may find the combination of SSL and HTTP Basic Auth to be sufficient. I would advise you not to use Basic Auth without SSL, as it is then easily sniffed. If you're utilizing Basic Auth, Taffy 1.3 added [getBasicAuthCredentials()](#getbasicauthcredentials).
If you're after something a bit more complex, but still short of OAuth, I've written an article covering [Advanced Authentication with Taffy](https://adamtuttle.codes/blog/2013/advanced-authentication-with-taffy-apis/).
Implementing OAuth is something I would like to document, but it is a fairly large topic and I haven't had the time yet. Pull requests welcome.
## Tomcat Idiosyncrasies
### 404 when your API is in a subdirectory
It's a known issue, and one we have no control over via code, that **on vanilla Tomcat** (as opposed to the modified version included in Adobe CF10+) **you'll probably find that you get a 404 error if you try to move your api into a subdirectory**. The 404 will complain that the URI is not found, such as:
```
/api/index.cfm/myResource
```
... Of course this is not found, because "index.cfm/myResource" isn't a file.
All hope is not lost, however!
**The Fix:** In your web.xml, you need to add an additional servlet mapping:
```xml
<servlet-mapping>
<servlet-name>CFMLServlet</servlet-name>
<url-pattern>/api/index.cfm/*</url-pattern>
</servlet-mapping>
```
This is because Tomcat doesn't support the use of _two_ wildcards in its mappings. You'll notice that installing ACF or Railo in Tomcat you'll get a web.xml with mappings that have a `url-pattern` of `index.cfm/*`, but unfortunately because of this limitation, you can't change that to `*/index.cfm/*`.
In the xml above you can see that I only have 1 wildcard, but to compensate I've specified the entire path to index.cfm, so that only 1 is needed. If you need to have multiple API's, you'll need a mapping for each index.cfm, and specify the full path of each. (Note that I used `/api/index.cfm` because it matched my example of a 404 for `/api/index.cfm/myResource`... yours should match the location of your index.cfm)
## Railo Idiosyncrasies
### Custom HTTP Status Messages
Taffy allows for setting the HTTP status message using [.withStatus()](#withstatus), such as:
```js
return representationOf({...}).withStatus(403, "Not Authorized");
```
Railo's default setup on Tomcat doesn't allow changing the Status Text header value; so you might use `.withStatus(403, "Account Past Due")` but Tomcat changes this back to `403 Not Authorized`.
You can either work around this by passing status information in the response body, or alternately changing **catalina.properties** (Tomcat config) to include the line:
```conf
org.apache.coyote.USE_CUSTOM_STATUS_MSG_IN_HEADER=true
```
[Hat tip to Jean-Bernard van Zuylen](https://github.com/atuttle/Taffy/issues/121#issuecomment-23410624)
### PUT requests & FORM scope
Prior to versions 4.1.1.002 and 4.2 of Railo, a PUT request would populate the FORM scope. This probably doesn't cause any issues with API's, but did cause a pair of unit tests to fail. (put_body_is_mime_content, and put_body_is_url_encoded_params) This is safe to ignore. Also, upgrade, man!
[Hat tip to Jean-Bernard van Zuylen](https://github.com/atuttle/Taffy/issues/121#issuecomment-23410624)
## More Guides
Some guides are too broad for this document. For your benefit, they are linked here:
- [Migration Guide](https://github.com/atuttle/Taffy/wiki/Migration-Guide) - Notes on upgrading from 1.3.x to 2.x.x
- [Create a dead-simple CRUD API](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Create-a-dead-simple-CRUD-API) walks you through creating your first Taffy-powered API
- [Authentication and Security](https://github.com/atuttle/Taffy/wiki/Authentication-and-Security)
- [Bean Factories](https://github.com/atuttle/Taffy/wiki/Bean-Factories)
- [Common API HTTP Status Codes](https://github.com/atuttle/Taffy/wiki/Common-API-HTTP-Status-Codes)
- [Custom Token Regular Expressions](https://github.com/atuttle/Taffy/wiki/Custom-token-regular-expressions)
- [Exception Logging Adapters](https://github.com/atuttle/Taffy/wiki/Exception-Logging-Adapters) describes integration with BugLogHQ and Hoth, and options for emailing yourself all exceptions
- [Organizing your resources into subfolders](https://github.com/atuttle/Taffy/wiki/Organizing-your-resources-into-subfolders) explains the caveats of using subfolders within /resources
- [URL Rewrite Rule Examples](https://github.com/atuttle/Taffy/wiki/URL-Rewrite-Rule-Examples)
- [Using a Custom Representation Class](https://github.com/atuttle/Taffy/wiki/Using-a-Custom-Representation-Class)
- [Rate Limiting your API](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Rate-Limit-access-to-your-API)
- [Require an API Key](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Require-an-API-Key)
- [Serialize data to a different data type](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Serialize-data-to-a-different-data-type)
- [Share Application variables with another application](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Share-application-variables-with-your-consumer-facing-application)
- [Support returning multiple data formats](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Support-returning-multiple-formats)
- [Allow file uploads to your API](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Upload-a-file-via-your-API)
- [Use an external bean factory to completely manage resources](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-use-an-external-bean-factory-like-coldspring-to-completely-manage-resources)
- [Use an external bean factory to resolve dependencies of your resources](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Use-an-external-bean-factory-like-ColdSpring-to-resolve-dependencies-of-your-resources)
- [Use ColdSpring AOP Advice for your resources](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Use-ColdSpring-AOP-Advice-for-your-resources)
- [Use JSONUtil instead of native JSON serialization](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Use-JSONUtil-instead-of-Native-JSON-serialization)
- [Use the autowire functionality of Taffy's factory to manage resource dependencies](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Use-Taffy's-built-in-Dependency-Injection-to-resolve-dependencies-of-your-resources)
- [Write your components using CF9 script-component syntax](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Write-your-components-using-CF9--script-component-syntax)
## Configuration Reference
### Configuration via Metadata
Metadata is used to apply configuration in a concise and elegant manor, where necessity and possibility intersect.
#### In Resources
Resources are CFCs that interpret a request and provide or update the requested data.
##### taffy:uri
The **taffy:uri** property applies to the `<cfcomponent>` tag or the `component{}` definition. It configures which URIs the CFC will respond to. There is no limit to the number of tokens that can be used in a single URI, as long as they are not consecutive and are delimited by at least one character (like a slash). Examples:
```xml
<cfcomponent taffy:uri="/artist/{artistId}">
</cfcomponent>
```
or
```js
component taffy_uri="/artist/{artistId}" {
}
```
The following example is _invalid_, because the tokens are not separated by (at least) a slash:<br/>
```
taffy:uri="/artist/{artistId}{artistName}"
```
##### Tokens
Tokens in URIs define the parts of the URI that are dynamic. They are identified by curly-brackets: `{tokenName}` and are passed **by name** to all functions that handle the request. The `{foo}` token's value will be passed to the `foo` argument of the associated method.
##### URI Matching Order
As of Taffy 1.3, URIs are processed into a sorted array on API startup, and searched in order for every request, such that `/artists/list` will match before `/artists/{id}`. You should design your API URI's accordingly. Pay special attention to placement and possible values for URI tokens. If a possible value is the same as a static URI, consider changing URI formatting. For example, if we had an artist with id "list", then having the static URI `/artists/list` would prevent the `/artists/{id}` URI from ever matching when the intention is to get the individual artist record for artist with id "list."
##### taffy:verb
By convention, resources will automatically map the 4 primary HTTP REST verbs -- GET, PUT, POST, DELETE -- to CFC methods with the same name. For extended verbs -- OPTIONS, HEAD -- or if you want to map one of the primary verbs to a method with a different name, you can use the `taffy:verb` metadata property on the method to specify the verb it should respond to. Examples:
```xml
<cffunction name="getUser" taffy:verb="get">
<cfargument name="userId" type="numeric" />
</cffunction>
```
or
```js
function getUser( numeric userId ) taffy_verb="get" {
}
```
#### In Representation Classes
Representation classes are used to take the data provided by a resource and serialize it into a format usable by the web service consumer. A single Representation Class is capable of serializing native data objects (strings, numbers, queries, structures, arrays, etc) into 1 or more formats. Typical formats include JSON, XML, or YAML, but are not limited.
##### taffy:mime
By convention, the mime-types supported by your API are determined by the method names in your default representation class. (The included default representation class supports only JSON.) Each `getAsX` method in your representation class describes a new mime type, defined in two parts: The X portion of the method name determines the extension of the mime type -- which can be appended to the URI as if it were a file, as in: `/artists/42.json` which would use `getAsJson`. Taffy will also return a content-type header with the content type that you supply in the `taffy:mime` metadata property of the `getAsX` method. Typically, this is something like "application/json" or "application/xml". Examples:
```xml
<cffunction name="getAsJson" taffy:mime="application/json">
</cffunction>
```
or
```js
function getAsJson() taffy_mime="application/json" {
}
```
##### taffy:default
When your API supports more than one data format (i.e. json and xml), you must set one as the default. You do this with the `taffy:default` property, which expects a boolean value of either TRUE or FALSE. The default is FALSE, and you never need to include `taffy:default="false"`. You only need to include `taffy:default="true"` on one method -- the one that should be the default. Examples:
```xml
<cffunction name="getAsJson" taffy:mime="application/json" taffy:default="true">
</cffunction>
<cffunction name="getAsXml" taffy:mime="application/xml">
</cffunction>
```
or
```js
function getAsJson() taffy_mime="application/json" taffy_default="true" {}
function getAsXml() taffy_mime="application/xml" {}
```
### variables.framework settings
Default values:
```js
variables.framework = {
reloadKey = "reload",
reloadPassword = "true",
reloadOnEveryRequest = false,
endpointURLParam = "endpoint",
representationClass = "taffy.core.nativeJsonRepresentation",
dashboardKey = "dashboard",
disableDashboard = false,
disabledDashboardRedirect = "",
jsonp = false,
unhandledPaths = "/flex2gateway",
allowCrossDomain = false,
globalHeaders = structNew(),
debugKey = "debug",
useEtags = false,
returnExceptionsAsJson = true,
exceptionLogAdapter = "taffy.bonus.LogToEmail",
exceptionLogAdapterConfig = {
emailFrom = "api-error@yourdomain.com",
emailTo = "you@yourdomain.com",
emailSubj = "Exception Trapped in API",
emailType = "html"
},
beanFactory = "",
environments = {}
};
```
#### reloadKey
**Available in:** Taffy 1.2+<br/>
**Type:** String<br/>
**Default:** "reload"<br/>
**Description:** Name of the url parameter that requests the framework to be reloaded. Used in combination with the reload password (see: reloadPassword), the framework will re-initialize itself. During re-initialization, all configuration settings are re-applied and all cached objects are cleared and reloaded. If the value of the key does not match the reload password, a reload will not be performed. This allows you to set a secret password to restrict control of reloading your API to trusted parties.
#### reloadPassword
**Available in:** Taffy 1.2+<br/>
**Type:** String<br/>
**Default:** "true"<br/>
**Description:** Accepted value of the url parameter that requests the framework to be reloaded. Used in combination with the reload key (see: reloadKey), the framework will re-initialize itself. During re-initialization, all configuration settings are re-applied and all cached objects are cleared and reloaded. If the value of the key does not match the reload password, a reload will not be performed. This allows you to set a secret password to restrict control of reloading your API to trusted parties.
#### reloadOnEveryRequest
**Available in:** Taffy 1.2+<br/>
**Type:** Boolean<br/>
**Default:** False<br/>
**Description:** Flag that indicates whether Taffy should reload cached values and configuration on every request. Useful in development; set to FALSE in production.
#### endpointURLParam
**Available in:** Taffy 1.3+<br/>
**Type:** String<br/>
**Default:** "endpoint"<br/>
**Description:** The query-string parameter name that can optionally be used to specify URI. Until now, URI formatting has been required to be index.cfm/URI; this parameter allows you to use index.cfm?endpoint=/URI. This setting (endpointURLParam) allows you to change the default parameter name of "endpoint" to something custom.
#### representationClass
**Available in:** Taffy 1.2+<br/>
**Type:** String<br/>
**Default:** "taffy.core.nativeJsonRepresentation"<br/>
**Description:** The CFC dot-notation path, or bean name, of the representation class that your API will use to serialize returned data for the client.
#### dashboardKey
**Available in:** Taffy 1.2+<br/>
**Deprecated **in: Taffy 1.3+<br/>
**Type:** String<br/>
**Default:** "dashboard"
Description: Name of the url parameter that displays the dashboard. The dashboard displays resources that your API is aware of, generates documentation about your API based on hint attributes, and contains a mock client to make testing your API easy.
#### disableDashboard
**Available in:** Taffy 1.2+<br/>
**Type:** Boolean<br/>
**Default:** False<br/>
**Description:** Whether or not Taffy will allow the dashboard to be displayed. If set to true, the dashboard key is simply ignored. You may wish to disable the dashboard in production, depending on whether or not you want customers/clients to be able to see it.
#### disabledDashboardRedirect
**Available in:** Taffy 1.3+<br/>
**Type:** String<br/>
**Default:** ""<br/>
**Description:** URL to which Taffy should redirect (302) the client/browser if the dashboard is disabled. If the dashboard is disabled and this value is blank, a simple 403 Forbidden response is sent instead.
#### jsonp
**Available in:** Taffy 2.0+<br/>
**Type:** Boolean/String<br/>
**Default:** false<br/>
**Description:** When false, JSONP is disabled. To enable it, change the value to a string, such as "callback". The value you specify will be the query parameter in which Taffy expects to find the JSONP callback name. Note: JSONP only works for GET requests (by design!)
#### unhandledPaths
**Available in:** Taffy 1.2+<br/>
**Type:** String (Comma-delimited list)<br/>
**Default:** "/flex2gateway"<br/>
**Description:** Set a list of paths (usually subfolders of the API) that you do not want Taffy to interfere with. Unless listed here, Taffy takes over the request lifecycle and does not execute the requested ColdFusion template.
#### allowCrossDomain
**Available in:** Taffy 1.2+<br/>
**Type:** Boolean<br/>
**Default:** False<br/>
**Description:** Whether or not to allow cross-domain access to your API. Turning this on adds the following headers:
```xml
<cfheader name="Access-Control-Allow-Origin" value="*" />
<cfheader name="Access-Control-Allow-Methods" value="#allowedVerbs#" />
<cfheader name="Access-Control-Allow-Headers" value="Content-Type" />
```
The allowed verbs, of course, are the ones allowed by the requested resource, as well as OPTIONS.
#### globalHeaders
**Available in:** Taffy 1.2+<br/>
**Type:** Structure<br/>
**Default:** {}<br/>
**Description:** A structure where each key is the name of a header you want to return, such as "X-MY-HEADER" and the structure value is the header value.
Global headers are static. You set them on application initialization and they do not change. If you need dynamic headers, you can add them to each response at runtime using withHeaders().
#### debugKey
**Available in:** Taffy 1.2+<br/>
**Type:** String<br/>
**Default:** "debug"<br/>
**Description:** Name of the url parameter that enables CF Debug Output.
#### useEtags
**Available in:** Taffy 1.3+<br/>
**Type:** Boolean<br/>
**Default:** False<br/>
**Description:** Enable the use of HTTP ETags for caching purposes. Taffy will automatically handle both sending the server ETag value and detecting client supplied ETags (via the If-None-Match header) for you; simply turn this setting on.
**NOTE FOR RAILO USERS:** While it will not cause errors, the underlying Java code used in this feature was improperly implemented prior to Railo 4.0.? and this could result in your result data being sent as if it were changed when it in fact has not. (I'm not sure which Railo point release will include the fix. The latest as of this writing is version 4.0.2, and does not include it.) Adobe ColdFusion is unaffected.
#### returnExceptionsAsJson
**Available in:** Taffy 1.2+<br/>
**Type:** Boolean<br/>
**Default:** true<br/>
**Description:** When an error occurs that is not otherwise handled, this option tells Taffy to attempt to format the error information as JSON and return that (regardless of the requested return format). As of Taffy 2.1 this also includes a structured stack trace with file names and line numbers.
#### exceptionLogAdapter
**Available in:** Taffy 1.2+<br/>
**Type:** String<br/>
**Default:** "taffy.bonus.LogToEmail"<br/>
**Description:** CFC dot-notation path to the exception logging adapter you want to use. Default adapter simply emails all exceptions. See [Exception Logging Adapters](https://github.com/atuttle/Taffy/wiki/Exception-Logging-Adapters) for more details.
#### exceptionLogAdapterConfig
**Available in:** Taffy 1.2+<br/>
**Type:** Any<br/>
**Default:** See defaults [here](#variablesframework-settings)<br/>
**Description:** Configuration that your chosen logging adapter requires. Can be any data type. See [Exception Logging Adapters](https://github.com/atuttle/Taffy/wiki/Exception-Logging-Adapters) for more details.
#### beanFactory
**Available in:** Taffy 1.2+<br/>
**Type:** Object Instance<br/>
**Default:** ""<br/>
**Description:** Already instantiated and cached (e.g. in Application scope) object instance of your external bean factory. Not required in order to use Taffy's built-in factory.
#### environments
**Available in:** Taffy 1.3+<br/>
**Type:** Structure<br/>
**Default:** {}<br/>
**Description:** Environment-specific overrides to any framework settings. Applied after general variables.framework settings, and after configureTaffy() has been called. See [getEnvironment()](#getenvironment) for more details.
## Index of API Methods
### Application.cfc
#### getPath()
**Parameters:** _(none)_
This method is provided as an extension point. On Adobe ColdFusion ("ACF") 9, installed in standard entire-server mode, no change should be necessary. However, if ACF is installed on another JEE app server (i.e. Tomcat, Glassfish, etc), or on JRun but using an EAR/WAR setup, then you may need to override this method to make Taffy work on your server.
#### getBasicAuthCredentials()
**Parameters:** _(none)_
**Added in Taffy 1.3.** This method returns a structure with two keys: `username`, and `password`. When the client does not provide HTTP Basic Auth credentials, the username and password keys will be blank. When the client does provide them, the values will be available in these keys.
This method is only available inside your Application.cfc. If, for example, you need access to the username in a resource, you can use this method inside onTaffyRequest and add the username to the requestArguments structure.
#### getBeanFactory()
**Parameters:** _(none)_
Returns whatever bean factory you may have set into Taffy, if any. If using the `/resources` folder, it returns Taffy's built-in factory.
#### newRepresentation()
**Use it inside:** onTaffyRequest<br/>
**Parameters:**
- **class (string)** - the dot-notation cfc path (or bean id if managing the class with a bean factory, including the built-in factory) for the representation class. Optional. Default value is the defined representation class. If you're using the framework default representation class, it's `nativeJsonRepresentation`.
Use this method inside onTaffyRequest when you want to _abort_ the request with some specific message or data. Use the `setData` method on the returned representation class to put your data/message into it before returning it.
#### getEnvironment()
**Parameters:** _(none)_
Taffy calls this method during initialization to determine in which configured environment, if any, it is executing. Overriding it is optional. Use whatever methodology you like to determine the result (e.g. hostname, reading a file or registry setting, etc.), and simply return, as string, the name of the current environment.
The returned value will be used to load environment-specific configuration. For example, if you have the following code in your Application.cfc, then the dashboard will be disabled in production:
```js
variables.framework = {
disableDashboard = false
,environments = {
production = {
disableDashboard = true
}
}
};
function getEnvironment(){
if (...){
return "production";
}else{
return "development";
}
}
```
#### onTaffyRequest()
**Parameters:**
- **verb (string)** - The HTTP request verb provided by the client
- **cfc (string)** - The CFC name (minus ".cfc") that would handle the request. (Bean Name, if using an external bean factory.)
- **requestArguments (struct)** - A structure containing all of the arguments of the request, including tokens from the URI as well as any query string parameters.
- **mimeExt (string)** - The mime extension (e.g. "json" - NOT the full mime type, e.g. "application/json")
- **headers (struct)** - A structure containing each header from the request, as sent by the client.
- **methodMetadata (struct)** - A structure containing any non-taffy metadata set on the requested resource method.
This method is optional, and allows you to inspect and potentially abort an API request in a way that adheres to the HTTP specification and REST best practices. If you choose not to override it (by implementing it in your Application.cfc), it will always return true, allowing the request to continue. If you implement it, you can check for things like an API key, or whether or not the customer has paid for your service, and return something other than the data that they are requesting.
If you do not return TRUE, allowing the request to continue as normal, then Taffy expects you to return a **[representation](#custom-representation-classes)** (either the default class, or a custom one) that it should immediately return to the consumer, serialized to the appropriate format. If you simply want to return with a status code of 403 (which indicates "Not Allowed"), you could do this:
```js
return newRepresentation().noData().withStatus(403);
```
Alternately, you could return some data to indicate that they owe you money or something:
```js
return newRepresentation()
.setData({error="Your account is past due. Please email accounts payable."})
.withStatus(403);
```
The options here are limited only by your imagination.
You can add data to the **requestArguments** structure and this will be passed on to any resource that handles the request. Simply add a key to the structure:
```js
function onTaffyRequest(verb, cfc, requestArguments, mimeExt, headers) {
arguments.requestArguments.myData = "myvalue";
return true;
}
```
In your resource:
```js
function get(myData) {
//arguments.myData => "myValue"
}
```
You can use the method metadata for anything you see fit; but the original use case was for role-based security. Consider the following resource method:
```js
public function getData( id ) taffy_method="get" role="datareader" { ... }
```
Taffy doesn't do anything with the **role** metadata on this method other than expose it to you in onTaffyRequest. So let's use the user's API key to find out what roles they have, and verify that the method's required role is among them. This is a snippet from your Application.cfc:
```js
function onTaffyRequest(verb, cfc, requestArgs, mime, head, methodMetadata){
local.user = (...); //get user from api key...
if (structKeyExists(methodMetadata, "role")){
for (var availableRole in local.user.roles){
if (availableRole == methodMetadata.role) { return true; }
}
return newRepresentation().noData().withStatus(403, "Not Authorized");
}else{
//no role required on the method, so allow anyone to use it
return true;
}
}
```
Here, if the user doesn't have the `datareader` role assigned, they'll get a 403 response.
### Resources
Resource CFCs extend `taffy.core.resource`. The following methods are available inside each of your Resource CFCs:
#### noData()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:** _(none)_
This method allows you to specify that there is no data to be returned for the current request. Generally, you would use it in conjunction with the **withStatus** method to set a specific return status for the request. For example, if the requested resource doesn't exist, you could return a 404 error like so:
```js
return noData().withStatus(404);
```
#### queryToArray()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **data (query)** - The query object to be transformed
This method transforms a ColdFusion query object into an array of structures. It was added because ColdFusion's serializeJSON functionality uses an ..._eccentric_... format for queries. **queryToArray** returns the format most people expect: a vanilla array of structures with named keys. To be fair the ACF serialization format uses less data as long as there is more than 1 row in the query, but it doesn't matter that you do a better job if nobody understands your output. _queryToArray also preserves query column name case, which serializeJSON does not._
```js
return representationOf(queryToArray(someQuery));
```
#### representationOf()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **data (any)** - The data to return to the consumer.
Data can be of any type, including complex data types like queries, structures, and arrays, as long as the serializer knows how to serialize them. For more information on using a custom representation class, see [Using a custom Representation Class](https://github.com/atuttle/Taffy/wiki/Using-a-Custom-Representation-Class).
#### saveLog()
**Use it inside:** anywhere inside a Resource CFC to log data using your [configured logging adapter](https://github.com/atuttle/Taffy/wiki/Exception-Logging-Adapters).<br/>
**Parameters:**
- **exception (struct)** - traditionally a CF exception object, but any struct may be passed.
What you pass to this method is simply handed off to the logging adapter. You may use one of the included adapters (LogToEmail, LogToBuglogHQ, or LogToHoth), or a custom logging adapter. If you write a custom logging adapter, it should implement the `taffy.bonus.ILogAdapter` interface.
If you don't configure a logging adapter, the default is LogToEmail, but the default `from` and `to` email addresses are not useful. See [Exception Log Adapters](https://github.com/atuttle/Taffy/wiki/Exception-Logging-Adapters) for more information on configuring logging adapters.
#### streamBinary()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **binaryData (any)** - the binary data to be streamed back to the consumer
Use this method in place of `representationOf()` to return a stream of binary data. Useful for streaming things like dynamically generated PDFs.
#### streamFile()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **fileName (string)** - fully qualified file path (eg c:\tmp\files.zip)
Use this method in place of `representationOf()` to stream a file from disk (or VFS). Optionally append `.andDelete( true )` to delete the file once streaming is complete.
```js
return streamFile("/foo.txt").andDelete(true);
```
#### streamImage()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **binaryData (any)** - the binary, base64 encoded data of the image, to be streamed back to the consumer
Use this method in place of `representationOf()` to stream an image from disk (or VFS).
#### withHeaders()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **headerStruct (struct)** - A structure whose keys are desired header names ("x-powered-by") and whose associated values are the values for the corresponding headers ("Taffy 1.1!").
This special method _**requires**_ the use of either **noData** or **representationOf**. It adds custom headers to the return. Additional use of **withStatus** optional.
```js
return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"});
```
#### withMime()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **mime (string)** - mime type (eg. "application/pdf") to be returned with the streamed file data
This special method _**requires**_ the use of either **streamFile** or **streamBinary**. It overrides the default mime type header for the return.
```js
return streamFile("kittens/cuteness.pdf").withMime("application/pdf");
```
#### withStatus()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **statusCode (numeric)** - the [HTTP Status Code](http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html) to return to the consumer.
- **statusText (string)** - the Status Text to include with the HTTP Status Code. The "OK" part of "200 OK"; the "Not Found" part of "404 Not Found".
This special method _**requires**_ the use of either **noData** or **representationOf**. It sets the HTTP Status Code of the return. Additional use of **withHeaders** optional.
_If you do not specify a return status code, Taffy will always return status code 200 (OK) by default._
```js
return noData().withStatus(404, "Not Found");
```
### Custom Representation Classes
Representation Classes have the job of converting the native data that your resources return (queries, structures, arrays, and so on...) into &mdash;usually&mdash; a string that can be sent to the client and used there. It's also possible to send images and binary files like PDFs and ZIPs, but that's a topic that has its own guide. Taffy comes with a representation class that uses ColdFusion's **serializeJson** method to convert native data to JSON strings, and uses this class by default.
_Why would you want to create a custom representation class, and how would you do it?_
#### Why
You want to create a Custom Representation Class ("CRC") if you're stymied by [the bugs in ColdFusion's serializeJson method](http://fusiongrokker.com/post/you-can-help-fix-coldfusion-json-serialization), or if you want to return something other than JSON: XML for example. Another reason to use a CRC is when your api should be capable of returning more than one return format &mdash; e.g. both XML and JSON.
#### How
A Custom Representation Class is a CFC that:
- Extends `taffy.core.baseRepresentation`
- Implements 1 or more "getAs{something}" methods
- ... that defines a `taffy:mime` attribute
Let's take a look at **taffy.core.nativeJsonRepresentation** &mdash; the default representation class that Taffy uses unless you configure something else.
```xml
<cfcomponent extends="taffy.core.baseRepresentation" output="false">
<cffunction
name="getAsJson"
output="false"
taffy:mime="application/json"
taffy:default="true"
hint="serializes data as JSON">
<cfreturn serializeJSON(variables.data) />
</cffunction>
</cfcomponent>
```
- Extends `taffy.core.baseRepresentation`? Check.
- Implements 1 or more "getAs{something}" methods? Check.
- ... that defines a `taffy:mime` attribute? Check.
The base class is easy to explain: It makes everything about CRC's work without you having to do anything. It's the black magic.
What's the `getAs{something}` method for? Well, it's for **get**ting the data out of your CRC in a format that the user is requesting. The client can specify its desired return format in one of two ways. An HTTP `Accept` header, or by appending `.{format}` to the URL.
The former looks like: `Accept: application/json`, and the latter looks like:
```
http://example.com/api/v1/index.cfm/users.json
```
You may be familiar or comfortable with either, but the point is that they both mean the same thing. Taffy looks at the methods in your CRC at startup to determine what types your API will support. It needs both the format-extension (.json, via `getAsJson`) and the mime type (application/json, via taffy:mime) in order to function properly.
When writing your own CRC, you can assume that a resource has returned data and the returned value is available to the CRC as `variables.data`. This is why the default class returns `serializeJson(variables.data)`. If your method is `getAsXML` then it needs to be able to convert the data into an XML string.
#### Multiple Formats
Since a common use-case for CRCs is supporting multiple data formats, we'll next look at **CustomRepresentationClass.cfc**, found in Taffy's **examples/api_twoFormats/resources** folder.
```xml
<cfcomponent extends="taffy.core.baseRepresentation">
<cfset variables.jsonUtil = application.jsonUtil />
<cfset variables.AnythingToXML = application.AnythingToXML />
<cffunction name="getAsJSON" taffy:mime="application/json" taffy:default="true" output="false">
<cfreturn variables.jsonUtil.serializeJson(variables.data) />
</cffunction>
<cffunction name="getAsXML" taffy:mime="application/xml" output="false">
<cfreturn variables.AnythingToXML.ToXML(variables.data) />
</cffunction>
</cfcomponent>
```
Here we see two getters: `getAsJSON` with `taffy:mime="application/json"` and `getAsXML` with `taffy:mime="application/xml"`. Each is using a 3rd party library (available in the example folder) to do the serialization work. It's assuming that these libraries are available in Application scope (check Application.cfc for the implementation). This is generally regarded as a bad practice ("tight coupling"), and Taffy does not require that your code be structured this way. It's only included this way to keep the example as simple and focused as possible.
When a request is made that wants XML back, Taffy calls the `getAsXML` method to get the XML string. When a request is made for JSON data, Taffy calls `getAsJSON`.
And that &mdash;in a nutshell&mdash; is Custom Representation Classes.
+801
View File
@@ -0,0 +1,801 @@
# Taffy
The REST Web Service framework for ColdFusion and Railo
<a id="download" href="https://github.com/atuttle/Taffy/archive/v2.2.0.zip">Download Taffy 2.2.0.zip</a>
- [Release Notes](https://github.com/atuttle/Taffy/releases/tag/v2.2.0)
- [GitHub](https://github.com/atuttle/Taffy)
- Join us in the **#taffy** chat room on the [CFML Slack](https://cfml-slack.herokuapp.com/)
## Quickstart: Your First API
**Application.cfc:**
```js
component extends="taffy.core.api" {}
```
**index.cfm:**
```html
<!-- this space left intentionally blank -->
```
**/resources/hello.cfc:**
```js
component extends="taffy.core.resource" taffy_uri="/hello" {
function get(){
return representationOf(['hello','world']);
}
}
```
Congratulations, you've just written your first API. Didn't think it could be so simple, did you? Indeed, [you can fit an entire Taffy API into a single tweet](https://twitter.com/cf_taffy/statuses/327415972581486592).
Point your browser to the empty **index.cfm** file you created, and have a look at the dashboard. Click the "hello" row to expand it, and click the **Send** button to make a REST request. The results of your request appear below the request parameters.
The response includes a **status code** and **status text**, as well as other **headers**, and the **response body** (if applicable). Taffy also shows you the amount of time the request took.
### Getting Started, with more details
Here's a [Getting Started guide](https://github.com/atuttle/Taffy/wiki/Getting-Started) that goes into a little bit more detail, if the above example wasn't enough.
## Installation Options
### Global /taffy Mapping
Best practice would be to keep Taffy _out of your web root_. This is also (almost) the easiest install method. Instead of putting taffy in the web root, use a global mapping (set in the CF Administrator) pointing `/taffy` to wherever you have it saved.
### /taffy in Web Root
Just put the /taffy folder at the root of your domain. Boom, done.
### Sub-folder
If you aren't allowed to create a global mapping, or for some other reason want to, you can install Taffy as a sub-folder of your API. (NB: This would also allow you to use multiple versions of Taffy on the same CF Instance.)
Using sub-folders requires the use of Application-Specific Mappings (introduced in Adobe ColdFusion 8). Start by creating this directory structure:
```
/path-to-your-api
├── Application.cfc
├── index.cfm
├─┬ /taffy
│ ├── /bonus
│ ├── /core
│ └── /dashboard
└─┬ /resources
├── ...
└── someResource.cfc
```
You can see that the taffy folder is a sibling to Application.cfc. This allows Application.cfc to use relative paths to extend `taffy.core.api`.
Next, if your Application.cfc and `/resources/` folder aren't in the web-root (e.g. they're inside something like `/api/`) then you'll need to add an [application-specific mapping](http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=appFramework_04.html) for `/resources` so that Taffy can find your resources to initialize the routes.
```js
this.mappings["/resources"] = expandPath("./resources");
```
You'll also need to add a mapping for `/taffy` so that the resources can extend `taffy.core.resource` (since the taffy folder isn't a child of the resources folder):
```js
this.mappings["/taffy"] = expandPath("./taffy");
```
## Authentication and Security
If your authentication and security requirements are simple, you may find the combination of SSL and HTTP Basic Auth to be sufficient. I would advise you not to use Basic Auth without SSL, as it is then easily sniffed. If you're utilizing Basic Auth, Taffy 1.3 added [getBasicAuthCredentials()](#getbasicauthcredentials).
If you're after something a bit more complex, but still short of OAuth, I've written an article covering [Advanced Authentication with Taffy](https://adamtuttle.codes/blog/2013/advanced-authentication-with-taffy-apis/).
Implementing OAuth is something I would like to document, but it is a fairly large topic and I haven't had the time yet. Pull requests welcome.
## Symlink Idiosyncrasies
Adobe ColdFusion (and possibly other engines) historically have been horrible about symlinks inside the web root; and will probably continue to be horrible about them in the future. It's best just to avoid them. They'll cause issues with more than just Taffy.
## Tomcat, JBoss (and other app server) Idiosyncrasies
### 404 when your API is in a subdirectory
It's a known issue, and one we have no control over via code, that **on vanilla Tomcat** (as opposed to the modified version included in Adobe CF10+) **you'll probably find that you get a 404 error if you try to move your api into a subdirectory**. The 404 will complain that the URI is not found, such as:
```
/api/index.cfm/myResource
```
... Of course this is not found, because "index.cfm/myResource" isn't a file.
All hope is not lost, however!
**The Fix:** In your web.xml, you need to add an additional servlet mapping:
```xml
<servlet-mapping>
<servlet-name>CFMLServlet</servlet-name>
<url-pattern>/api/index.cfm/*</url-pattern>
</servlet-mapping>
```
This is because Tomcat doesn't support the use of _two_ wildcards in its mappings. You'll notice that installing ACF or Railo in Tomcat you'll get a web.xml with mappings that have a `url-pattern` of `index.cfm/*`, but unfortunately because of this limitation, you can't change that to `*/index.cfm/*`.
In the xml above you can see that I only have 1 wildcard, but to compensate I've specified the entire path to index.cfm, so that only 1 is needed. If you need to have multiple API's, you'll need a mapping for each index.cfm, and specify the full path of each. (Note that I used `/api/index.cfm` because it matched my example of a 404 for `/api/index.cfm/myResource`... yours should match the location of your index.cfm)
## Railo Idiosyncrasies
### Custom HTTP Status Messages
Taffy allows for setting the HTTP status message using [.withStatus()](#withstatus), such as:
```js
return representationOf({...}).withStatus(403, "Not Authorized");
```
Railo's default setup on Tomcat doesn't allow changing the Status Text header value; so you might use `.withStatus(403, "Account Past Due")` but Tomcat changes this back to `403 Not Authorized`.
You can either work around this by passing status information in the response body, or alternately changing **catalina.properties** (Tomcat config) to include the line:
```conf
org.apache.coyote.USE_CUSTOM_STATUS_MSG_IN_HEADER=true
```
[Hat tip to Jean-Bernard van Zuylen](https://github.com/atuttle/Taffy/issues/121#issuecomment-23410624)
### PUT requests & FORM scope
Prior to versions 4.1.1.002 and 4.2 of Railo, a PUT request would populate the FORM scope. This probably doesn't cause any issues with API's, but did cause a pair of unit tests to fail. (put_body_is_mime_content, and put_body_is_url_encoded_params) This is safe to ignore. Also, upgrade, man!
[Hat tip to Jean-Bernard van Zuylen](https://github.com/atuttle/Taffy/issues/121#issuecomment-23410624)
## More Guides
Some guides are too broad for this document. For your benefit, they are linked here:
- [Migration Guide](https://github.com/atuttle/Taffy/wiki/Migration-Guide) - Notes on upgrading from 1.3.x to 2.x.x
- [Create a dead-simple CRUD API](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Create-a-dead-simple-CRUD-API) walks you through creating your first Taffy-powered API
- [Authentication and Security](https://github.com/atuttle/Taffy/wiki/Authentication-and-Security)
- [Bean Factories](https://github.com/atuttle/Taffy/wiki/Bean-Factories)
- [Common API HTTP Status Codes](https://github.com/atuttle/Taffy/wiki/Common-API-HTTP-Status-Codes)
- [Custom Token Regular Expressions](https://github.com/atuttle/Taffy/wiki/Custom-token-regular-expressions)
- [Exception Logging Adapters](https://github.com/atuttle/Taffy/wiki/Exception-Logging-Adapters) describes integration with BugLogHQ and Hoth, and options for emailing yourself all exceptions
- [Organizing your resources into subfolders](https://github.com/atuttle/Taffy/wiki/Organizing-your-resources-into-subfolders) explains the caveats of using subfolders within /resources
- [URL Rewrite Rule Examples](https://github.com/atuttle/Taffy/wiki/URL-Rewrite-Rule-Examples)
- [Using a Custom Representation Class](https://github.com/atuttle/Taffy/wiki/Using-a-Custom-Representation-Class)
- [Rate Limiting your API](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Rate-Limit-access-to-your-API)
- [Require an API Key](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Require-an-API-Key)
- [Serialize data to a different data type](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Serialize-data-to-a-different-data-type)
- [Share Application variables with another application](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Share-application-variables-with-your-consumer-facing-application)
- [Support returning multiple data formats](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Support-returning-multiple-formats)
- [Allow file uploads to your API](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Upload-a-file-via-your-API)
- [Use an external bean factory to completely manage resources](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-use-an-external-bean-factory-like-coldspring-to-completely-manage-resources)
- [Use an external bean factory to resolve dependencies of your resources](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Use-an-external-bean-factory-like-ColdSpring-to-resolve-dependencies-of-your-resources)
- [Use ColdSpring AOP Advice for your resources](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Use-ColdSpring-AOP-Advice-for-your-resources)
- [Use JSONUtil instead of native JSON serialization](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Use-JSONUtil-instead-of-Native-JSON-serialization)
- [Use the autowire functionality of Taffy's factory to manage resource dependencies](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Use-Taffy's-built-in-Dependency-Injection-to-resolve-dependencies-of-your-resources)
- [Write your components using CF9 script-component syntax](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Write-your-components-using-CF9--script-component-syntax)
## Configuration Reference
### Configuration via Metadata
Metadata is used to apply configuration in a concise and elegant manor, where necessity and possibility intersect.
#### In Resources
Resources are CFCs that interpret a request and provide or update the requested data.
##### taffy:uri
The **taffy:uri** property applies to the `<cfcomponent>` tag or the `component{}` definition. It configures which URIs the CFC will respond to. There is no limit to the number of tokens that can be used in a single URI, as long as they are not consecutive and are delimited by at least one character (like a slash). Examples:
```xml
<cfcomponent taffy:uri="/artist/{artistId}">
</cfcomponent>
```
or
```js
component taffy_uri="/artist/{artistId}" {
}
```
The following example is _invalid_, because the tokens are not separated by (at least) a slash:<br/>
```
taffy:uri="/artist/{artistId}{artistName}"
```
##### Tokens
Tokens in URIs define the parts of the URI that are dynamic. They are identified by curly-brackets: `{tokenName}` and are passed **by name** to all functions that handle the request. The `{foo}` token's value will be passed to the `foo` argument of the associated method.
##### URI Matching Order
As of Taffy 1.3, URIs are processed into a sorted array on API startup, and searched in order for every request, such that `/artists/list` will match before `/artists/{id}`. You should design your API URI's accordingly. Pay special attention to placement and possible values for URI tokens. If a possible value is the same as a static URI, consider changing URI formatting. For example, if we had an artist with id "list", then having the static URI `/artists/list` would prevent the `/artists/{id}` URI from ever matching when the intention is to get the individual artist record for artist with id "list."
##### taffy:verb
By convention, resources will automatically map the 4 primary HTTP REST verbs -- GET, PUT, POST, DELETE -- to CFC methods with the same name. For extended verbs -- OPTIONS, HEAD -- or if you want to map one of the primary verbs to a method with a different name, you can use the `taffy:verb` metadata property on the method to specify the verb it should respond to. Examples:
```xml
<cffunction name="getUser" taffy:verb="get">
<cfargument name="userId" type="numeric" />
</cffunction>
```
or
```js
function getUser( numeric userId ) taffy_verb="get" {
}
```
#### In Representation Classes
Representation classes are used to take the data provided by a resource and serialize it into a format usable by the web service consumer. A single Representation Class is capable of serializing native data objects (strings, numbers, queries, structures, arrays, etc) into 1 or more formats. Typical formats include JSON, XML, or YAML, but are not limited.
##### taffy:mime
By convention, the mime-types supported by your API are determined by the method names in your default representation class. (The included default representation class supports only JSON.) Each `getAsX` method in your representation class describes a new mime type, defined in two parts: The X portion of the method name determines the extension of the mime type -- which can be appended to the URI as if it were a file, as in: `/artists/42.json` which would use `getAsJson`. Taffy will also return a content-type header with the content type that you supply in the `taffy:mime` metadata property of the `getAsX` method. Typically, this is something like "application/json" or "application/xml". Examples:
```xml
<cffunction name="getAsJson" taffy:mime="application/json">
</cffunction>
```
or
```js
function getAsJson() taffy_mime="application/json" {
}
```
##### taffy:default
When your API supports more than one data format (i.e. json and xml), you must set one as the default. You do this with the `taffy:default` property, which expects a boolean value of either TRUE or FALSE. The default is FALSE, and you never need to include `taffy:default="false"`. You only need to include `taffy:default="true"` on one method -- the one that should be the default. Examples:
```xml
<cffunction name="getAsJson" taffy:mime="application/json" taffy:default="true">
</cffunction>
<cffunction name="getAsXml" taffy:mime="application/xml">
</cffunction>
```
or
```js
function getAsJson() taffy_mime="application/json" taffy_default="true" {}
function getAsXml() taffy_mime="application/xml" {}
```
### variables.framework settings
Default values:
```js
variables.framework = {
reloadKey = "reload",
reloadPassword = "true",
reloadOnEveryRequest = false,
endpointURLParam = "endpoint",
representationClass = "taffy.core.nativeJsonRepresentation",
dashboardKey = "dashboard",
disableDashboard = false,
disabledDashboardRedirect = "",
jsonp = false,
unhandledPaths = "/flex2gateway",
allowCrossDomain = false,
globalHeaders = structNew(),
debugKey = "debug",
useEtags = false,
returnExceptionsAsJson = true,
exceptionLogAdapter = "taffy.bonus.LogToEmail",
exceptionLogAdapterConfig = {
emailFrom = "api-error@yourdomain.com",
emailTo = "you@yourdomain.com",
emailSubj = "Exception Trapped in API",
emailType = "html"
},
beanFactory = "",
environments = {}
};
```
#### reloadKey
**Available in:** Taffy 1.2+<br/>
**Type:** String<br/>
**Default:** "reload"<br/>
**Description:** Name of the url parameter that requests the framework to be reloaded. Used in combination with the reload password (see: reloadPassword), the framework will re-initialize itself. During re-initialization, all configuration settings are re-applied and all cached objects are cleared and reloaded. If the value of the key does not match the reload password, a reload will not be performed. This allows you to set a secret password to restrict control of reloading your API to trusted parties.
#### reloadPassword
**Available in:** Taffy 1.2+<br/>
**Type:** String<br/>
**Default:** "true"<br/>
**Description:** Accepted value of the url parameter that requests the framework to be reloaded. Used in combination with the reload key (see: reloadKey), the framework will re-initialize itself. During re-initialization, all configuration settings are re-applied and all cached objects are cleared and reloaded. If the value of the key does not match the reload password, a reload will not be performed. This allows you to set a secret password to restrict control of reloading your API to trusted parties.
#### reloadOnEveryRequest
**Available in:** Taffy 1.2+<br/>
**Type:** Boolean<br/>
**Default:** False<br/>
**Description:** Flag that indicates whether Taffy should reload cached values and configuration on every request. Useful in development; set to FALSE in production.
#### endpointURLParam
**Available in:** Taffy 1.3+<br/>
**Type:** String<br/>
**Default:** "endpoint"<br/>
**Description:** The query-string parameter name that can optionally be used to specify URI. Until now, URI formatting has been required to be index.cfm/URI; this parameter allows you to use index.cfm?endpoint=/URI. This setting (endpointURLParam) allows you to change the default parameter name of "endpoint" to something custom.
#### representationClass
**Available in:** Taffy 1.2+<br/>
**Type:** String<br/>
**Default:** "taffy.core.nativeJsonRepresentation"<br/>
**Description:** The CFC dot-notation path, or bean name, of the representation class that your API will use to serialize returned data for the client.
#### dashboardKey
**Available in:** Taffy 1.2+<br/>
**Deprecated **in: Taffy 1.3+<br/>
**Type:** String<br/>
**Default:** "dashboard"
Description: Name of the url parameter that displays the dashboard. The dashboard displays resources that your API is aware of, generates documentation about your API based on hint attributes, and contains a mock client to make testing your API easy.
#### disableDashboard
**Available in:** Taffy 1.2+<br/>
**Type:** Boolean<br/>
**Default:** False<br/>
**Description:** Whether or not Taffy will allow the dashboard to be displayed. If set to true, the dashboard key is simply ignored. You may wish to disable the dashboard in production, depending on whether or not you want customers/clients to be able to see it.
#### disabledDashboardRedirect
**Available in:** Taffy 1.3+<br/>
**Type:** String<br/>
**Default:** ""<br/>
**Description:** URL to which Taffy should redirect (302) the client/browser if the dashboard is disabled. If the dashboard is disabled and this value is blank, a simple 403 Forbidden response is sent instead.
#### jsonp
**Available in:** Taffy 2.0+<br/>
**Type:** Boolean/String<br/>
**Default:** false<br/>
**Description:** When false, JSONP is disabled. To enable it, change the value to a string, such as "callback". The value you specify will be the query parameter in which Taffy expects to find the JSONP callback name. Note: JSONP only works for GET requests (by design!)
#### unhandledPaths
**Available in:** Taffy 1.2+<br/>
**Type:** String (Comma-delimited list)<br/>
**Default:** "/flex2gateway"<br/>
**Description:** Set a list of paths (usually subfolders of the API) that you do not want Taffy to interfere with. Unless listed here, Taffy takes over the request lifecycle and does not execute the requested ColdFusion template.
#### allowCrossDomain
**Available in:** Taffy 1.2+<br/>
**Type:** Boolean<br/>
**Default:** False<br/>
**Description:** Whether or not to allow cross-domain access to your API. Turning this on adds the following headers:
```xml
<cfheader name="Access-Control-Allow-Origin" value="*" />
<cfheader name="Access-Control-Allow-Methods" value="#allowedVerbs#" />
<cfheader name="Access-Control-Allow-Headers" value="Content-Type" />
```
The allowed verbs, of course, are the ones allowed by the requested resource, as well as OPTIONS.
#### globalHeaders
**Available in:** Taffy 1.2+<br/>
**Type:** Structure<br/>
**Default:** {}<br/>
**Description:** A structure where each key is the name of a header you want to return, such as "X-MY-HEADER" and the structure value is the header value.
Global headers are static. You set them on application initialization and they do not change. If you need dynamic headers, you can add them to each response at runtime using withHeaders().
#### debugKey
**Available in:** Taffy 1.2+<br/>
**Type:** String<br/>
**Default:** "debug"<br/>
**Description:** Name of the url parameter that enables CF Debug Output.
#### useEtags
**Available in:** Taffy 1.3+<br/>
**Type:** Boolean<br/>
**Default:** False<br/>
**Description:** Enable the use of HTTP ETags for caching purposes. Taffy will automatically handle both sending the server ETag value and detecting client supplied ETags (via the If-None-Match header) for you; simply turn this setting on.
**NOTE FOR RAILO USERS:** While it will not cause errors, the underlying Java code used in this feature was improperly implemented prior to Railo 4.0.? and this could result in your result data being sent as if it were changed when it in fact has not. (I'm not sure which Railo point release will include the fix. The latest as of this writing is version 4.0.2, and does not include it.) Adobe ColdFusion is unaffected.
#### returnExceptionsAsJson
**Available in:** Taffy 1.2+<br/>
**Type:** Boolean<br/>
**Default:** true<br/>
**Description:** When an error occurs that is not otherwise handled, this option tells Taffy to attempt to format the error information as JSON and return that (regardless of the requested return format). As of Taffy 2.1 this also includes a structured stack trace with file names and line numbers.
#### exceptionLogAdapter
**Available in:** Taffy 1.2+<br/>
**Type:** String<br/>
**Default:** "taffy.bonus.LogToEmail"<br/>
**Description:** CFC dot-notation path to the exception logging adapter you want to use. Default adapter simply emails all exceptions. See [Exception Logging Adapters](https://github.com/atuttle/Taffy/wiki/Exception-Logging-Adapters) for more details.
#### exceptionLogAdapterConfig
**Available in:** Taffy 1.2+<br/>
**Type:** Any<br/>
**Default:** See defaults [here](#variablesframework-settings)<br/>
**Description:** Configuration that your chosen logging adapter requires. Can be any data type. See [Exception Logging Adapters](https://github.com/atuttle/Taffy/wiki/Exception-Logging-Adapters) for more details.
#### beanFactory
**Available in:** Taffy 1.2+<br/>
**Type:** Object Instance<br/>
**Default:** ""<br/>
**Description:** Already instantiated and cached (e.g. in Application scope) object instance of your external bean factory. Not required in order to use Taffy's built-in factory.
#### environments
**Available in:** Taffy 1.3+<br/>
**Type:** Structure<br/>
**Default:** {}<br/>
**Description:** Environment-specific overrides to any framework settings. Applied after general variables.framework settings, and after configureTaffy() has been called. See [getEnvironment()](#getenvironment) for more details.
## Index of API Methods
### Application.cfc
#### getPath()
**Parameters:** _(none)_
This method is provided as an extension point. On Adobe ColdFusion ("ACF") 9, installed in standard entire-server mode, no change should be necessary. However, if ACF is installed on another JEE app server (i.e. Tomcat, Glassfish, etc), or on JRun but using an EAR/WAR setup, then you may need to override this method to make Taffy work on your server.
#### getBasicAuthCredentials()
**Parameters:** _(none)_
**Added in Taffy 1.3.** This method returns a structure with two keys: `username`, and `password`. When the client does not provide HTTP Basic Auth credentials, the username and password keys will be blank. When the client does provide them, the values will be available in these keys.
This method is only available inside your Application.cfc. If, for example, you need access to the username in a resource, you can use this method inside onTaffyRequest and add the username to the requestArguments structure.
#### getBeanFactory()
**Parameters:** _(none)_
Returns whatever bean factory you may have set into Taffy, if any. If using the `/resources` folder, it returns Taffy's built-in factory.
#### newRepresentation()
**Use it inside:** onTaffyRequest<br/>
**Parameters:**
- **class (string)** - the dot-notation cfc path (or bean id if managing the class with a bean factory, including the built-in factory) for the representation class. Optional. Default value is the defined representation class. If you're using the framework default representation class, it's `nativeJsonRepresentation`.
Use this method inside onTaffyRequest when you want to _abort_ the request with some specific message or data. Use the `setData` method on the returned representation class to put your data/message into it before returning it.
#### getEnvironment()
**Parameters:** _(none)_
Taffy calls this method during initialization to determine in which configured environment, if any, it is executing. Overriding it is optional. Use whatever methodology you like to determine the result (e.g. hostname, reading a file or registry setting, etc.), and simply return, as string, the name of the current environment.
The returned value will be used to load environment-specific configuration. For example, if you have the following code in your Application.cfc, then the dashboard will be disabled in production:
```js
variables.framework = {
disableDashboard = false
,environments = {
production = {
disableDashboard = true
}
}
};
function getEnvironment(){
if (...){
return "production";
}else{
return "development";
}
}
```
#### onTaffyRequest()
**Parameters:**
- **verb (string)** - The HTTP request verb provided by the client
- **cfc (string)** - The CFC name (minus ".cfc") that would handle the request. (Bean Name, if using an external bean factory.)
- **requestArguments (struct)** - A structure containing all of the arguments of the request, including tokens from the URI as well as any query string parameters.
- **mimeExt (string)** - The mime extension (e.g. "json" - NOT the full mime type, e.g. "application/json")
- **headers (struct)** - A structure containing each header from the request, as sent by the client.
- **methodMetadata (struct)** - A structure containing any non-taffy metadata set on the requested resource method.
This method is optional, and allows you to inspect and potentially abort an API request in a way that adheres to the HTTP specification and REST best practices. If you choose not to override it (by implementing it in your Application.cfc), it will always return true, allowing the request to continue. If you implement it, you can check for things like an API key, or whether or not the customer has paid for your service, and return something other than the data that they are requesting.
If you do not return TRUE, allowing the request to continue as normal, then Taffy expects you to return a **[representation](#custom-representation-classes)** (either the default class, or a custom one) that it should immediately return to the consumer, serialized to the appropriate format. If you simply want to return with a status code of 403 (which indicates "Not Allowed"), you could do this:
```js
return newRepresentation().noData().withStatus(403);
```
Alternately, you could return some data to indicate that they owe you money or something:
```js
return newRepresentation()
.setData({error="Your account is past due. Please email accounts payable."})
.withStatus(403);
```
The options here are limited only by your imagination.
You can add data to the **requestArguments** structure and this will be passed on to any resource that handles the request. Simply add a key to the structure:
```js
function onTaffyRequest(verb, cfc, requestArguments, mimeExt, headers) {
arguments.requestArguments.myData = "myvalue";
return true;
}
```
In your resource:
```js
function get(myData) {
//arguments.myData => "myValue"
}
```
You can use the method metadata for anything you see fit; but the original use case was for role-based security. Consider the following resource method:
```js
public function getData( id ) taffy_method="get" role="datareader" { ... }
```
Taffy doesn't do anything with the **role** metadata on this method other than expose it to you in onTaffyRequest. So let's use the user's API key to find out what roles they have, and verify that the method's required role is among them. This is a snippet from your Application.cfc:
```js
function onTaffyRequest(verb, cfc, requestArgs, mime, head, methodMetadata){
local.user = (...); //get user from api key...
if (structKeyExists(methodMetadata, "role")){
for (var availableRole in local.user.roles){
if (availableRole == methodMetadata.role) { return true; }
}
return newRepresentation().noData().withStatus(403, "Not Authorized");
}else{
//no role required on the method, so allow anyone to use it
return true;
}
}
```
Here, if the user doesn't have the `datareader` role assigned, they'll get a 403 response.
### Resources
Resource CFCs extend `taffy.core.resource`. The following methods are available inside each of your Resource CFCs:
#### noData()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:** _(none)_
This method allows you to specify that there is no data to be returned for the current request. Generally, you would use it in conjunction with the **withStatus** method to set a specific return status for the request. For example, if the requested resource doesn't exist, you could return a 404 error like so:
```js
return noData().withStatus(404);
```
#### queryToArray()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **data (query)** - The query object to be transformed
This method transforms a ColdFusion query object into an array of structures. It was added because ColdFusion's serializeJSON functionality uses an ..._eccentric_... format for queries. **queryToArray** returns the format most people expect: a vanilla array of structures with named keys. To be fair the ACF serialization format uses less data as long as there is more than 1 row in the query, but it doesn't matter that you do a better job if nobody understands your output. _queryToArray also preserves query column name case, which serializeJSON does not._
```js
return representationOf(queryToArray(someQuery));
```
#### representationOf()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **data (any)** - The data to return to the consumer.
Data can be of any type, including complex data types like queries, structures, and arrays, as long as the serializer knows how to serialize them. For more information on using a custom representation class, see [Using a custom Representation Class](https://github.com/atuttle/Taffy/wiki/Using-a-Custom-Representation-Class).
#### saveLog()
**Use it inside:** anywhere inside a Resource CFC to log data using your [configured logging adapter](https://github.com/atuttle/Taffy/wiki/Exception-Logging-Adapters).<br/>
**Parameters:**
- **exception (struct)** - traditionally a CF exception object, but any struct may be passed.
What you pass to this method is simply handed off to the logging adapter. You may use one of the included adapters (LogToEmail, LogToBuglogHQ, or LogToHoth), or a custom logging adapter. If you write a custom logging adapter, it should implement the `taffy.bonus.ILogAdapter` interface.
If you don't configure a logging adapter, the default is LogToEmail, but the default `from` and `to` email addresses are not useful. See [Exception Log Adapters](https://github.com/atuttle/Taffy/wiki/Exception-Logging-Adapters) for more information on configuring logging adapters.
#### streamBinary()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **binaryData (any)** - the binary data to be streamed back to the consumer
Use this method in place of `representationOf()` to return a stream of binary data. Useful for streaming things like dynamically generated PDFs.
#### streamFile()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **fileName (string)** - fully qualified file path (eg c:\tmp\files.zip)
Use this method in place of `representationOf()` to stream a file from disk (or VFS). Optionally append `.andDelete( true )` to delete the file once streaming is complete.
```js
return streamFile("/foo.txt").andDelete(true);
```
#### streamImage()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **binaryData (any)** - the binary, base64 encoded data of the image, to be streamed back to the consumer
Use this method in place of `representationOf()` to stream an image from disk (or VFS).
#### withHeaders()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **headerStruct (struct)** - A structure whose keys are desired header names ("x-powered-by") and whose associated values are the values for the corresponding headers ("Taffy 1.1!").
This special method _**requires**_ the use of either **noData** or **representationOf**. It adds custom headers to the return. Additional use of **withStatus** optional.
```js
return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"});
```
#### withMime()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **mime (string)** - mime type (eg. "application/pdf") to be returned with the streamed file data
This special method _**requires**_ the use of either **streamFile** or **streamBinary**. It overrides the default mime type header for the return.
```js
return streamFile("kittens/cuteness.pdf").withMime("application/pdf");
```
#### withStatus()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **statusCode (numeric)** - the [HTTP Status Code](http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html) to return to the consumer.
- **statusText (string)** - the Status Text to include with the HTTP Status Code. The "OK" part of "200 OK"; the "Not Found" part of "404 Not Found".
This special method _**requires**_ the use of either **noData** or **representationOf**. It sets the HTTP Status Code of the return. Additional use of **withHeaders** optional.
_If you do not specify a return status code, Taffy will always return status code 200 (OK) by default._
```js
return noData().withStatus(404, "Not Found");
```
### Custom Representation Classes
Representation Classes have the job of converting the native data that your resources return (queries, structures, arrays, and so on...) into &mdash;usually&mdash; a string that can be sent to the client and used there. It's also possible to send images and binary files like PDFs and ZIPs, but that's a topic that has its own guide. Taffy comes with a representation class that uses ColdFusion's **serializeJson** method to convert native data to JSON strings, and uses this class by default.
_Why would you want to create a custom representation class, and how would you do it?_
#### Why
You want to create a Custom Representation Class ("CRC") if you're stymied by [the bugs in ColdFusion's serializeJson method](http://fusiongrokker.com/post/you-can-help-fix-coldfusion-json-serialization), or if you want to return something other than JSON: XML for example. Another reason to use a CRC is when your api should be capable of returning more than one return format &mdash; e.g. both XML and JSON.
#### How
A Custom Representation Class is a CFC that:
- Extends `taffy.core.baseRepresentation`
- Implements 1 or more "getAs{something}" methods
- ... that defines a `taffy:mime` attribute
Let's take a look at **taffy.core.nativeJsonRepresentation** &mdash; the default representation class that Taffy uses unless you configure something else.
```xml
<cfcomponent extends="taffy.core.baseRepresentation" output="false">
<cffunction
name="getAsJson"
output="false"
taffy:mime="application/json"
taffy:default="true"
hint="serializes data as JSON">
<cfreturn serializeJSON(variables.data) />
</cffunction>
</cfcomponent>
```
- Extends `taffy.core.baseRepresentation`? Check.
- Implements 1 or more "getAs{something}" methods? Check.
- ... that defines a `taffy:mime` attribute? Check.
The base class is easy to explain: It makes everything about CRC's work without you having to do anything. It's the black magic.
What's the `getAs{something}` method for? Well, it's for **get**ting the data out of your CRC in a format that the user is requesting. The client can specify its desired return format in one of two ways. An HTTP `Accept` header, or by appending `.{format}` to the URL.
The former looks like: `Accept: application/json`, and the latter looks like:
```
http://example.com/api/v1/index.cfm/users.json
```
You may be familiar or comfortable with either, but the point is that they both mean the same thing. Taffy looks at the methods in your CRC at startup to determine what types your API will support. It needs both the format-extension (.json, via `getAsJson`) and the mime type (application/json, via taffy:mime) in order to function properly.
When writing your own CRC, you can assume that a resource has returned data and the returned value is available to the CRC as `variables.data`. This is why the default class returns `serializeJson(variables.data)`. If your method is `getAsXML` then it needs to be able to convert the data into an XML string.
#### Multiple Formats
Since a common use-case for CRCs is supporting multiple data formats, we'll next look at **CustomRepresentationClass.cfc**, found in Taffy's **examples/api_twoFormats/resources** folder.
```xml
<cfcomponent extends="taffy.core.baseRepresentation">
<cfset variables.jsonUtil = application.jsonUtil />
<cfset variables.AnythingToXML = application.AnythingToXML />
<cffunction name="getAsJSON" taffy:mime="application/json" taffy:default="true" output="false">
<cfreturn variables.jsonUtil.serializeJson(variables.data) />
</cffunction>
<cffunction name="getAsXML" taffy:mime="application/xml" output="false">
<cfreturn variables.AnythingToXML.ToXML(variables.data) />
</cffunction>
</cfcomponent>
```
Here we see two getters: `getAsJSON` with `taffy:mime="application/json"` and `getAsXML` with `taffy:mime="application/xml"`. Each is using a 3rd party library (available in the example folder) to do the serialization work. It's assuming that these libraries are available in Application scope (check Application.cfc for the implementation). This is generally regarded as a bad practice ("tight coupling"), and Taffy does not require that your code be structured this way. It's only included this way to keep the example as simple and focused as possible.
When a request is made that wants XML back, Taffy calls the `getAsXML` method to get the XML string. When a request is made for JSON data, Taffy calls `getAsJSON`.
And that &mdash;in a nutshell&mdash; is Custom Representation Classes.
+801
View File
@@ -0,0 +1,801 @@
# Taffy
The REST Web Service framework for ColdFusion and Railo
<a id="download" href="https://github.com/atuttle/Taffy/archive/v2.2.3.zip">Download Taffy 2.2.3.zip</a>
- [Release Notes](https://github.com/atuttle/Taffy/releases/tag/v2.2.3)
- [GitHub](https://github.com/atuttle/Taffy)
- Join us in the **#taffy** chat room on the [CFML Slack](https://cfml-slack.herokuapp.com/)
## Quickstart: Your First API
**Application.cfc:**
```js
component extends="taffy.core.api" {}
```
**index.cfm:**
```html
<!-- this space left intentionally blank -->
```
**/resources/hello.cfc:**
```js
component extends="taffy.core.resource" taffy_uri="/hello" {
function get(){
return representationOf(['hello','world']);
}
}
```
Congratulations, you've just written your first API. Didn't think it could be so simple, did you? Indeed, [you can fit an entire Taffy API into a single tweet](https://twitter.com/cf_taffy/statuses/327415972581486592).
Point your browser to the empty **index.cfm** file you created, and have a look at the dashboard. Click the "hello" row to expand it, and click the **Send** button to make a REST request. The results of your request appear below the request parameters.
The response includes a **status code** and **status text**, as well as other **headers**, and the **response body** (if applicable). Taffy also shows you the amount of time the request took.
### Getting Started, with more details
Here's a [Getting Started guide](https://github.com/atuttle/Taffy/wiki/Getting-Started) that goes into a little bit more detail, if the above example wasn't enough.
## Installation Options
### Global /taffy Mapping
Best practice would be to keep Taffy _out of your web root_. This is also (almost) the easiest install method. Instead of putting taffy in the web root, use a global mapping (set in the CF Administrator) pointing `/taffy` to wherever you have it saved.
### /taffy in Web Root
Just put the /taffy folder at the root of your domain. Boom, done.
### Sub-folder
If you aren't allowed to create a global mapping, or for some other reason want to, you can install Taffy as a sub-folder of your API. (NB: This would also allow you to use multiple versions of Taffy on the same CF Instance.)
Using sub-folders requires the use of Application-Specific Mappings (introduced in Adobe ColdFusion 8). Start by creating this directory structure:
```
/path-to-your-api
├── Application.cfc
├── index.cfm
├─┬ /taffy
│ ├── /bonus
│ ├── /core
│ └── /dashboard
└─┬ /resources
├── ...
└── someResource.cfc
```
You can see that the taffy folder is a sibling to Application.cfc. This allows Application.cfc to use relative paths to extend `taffy.core.api`.
Next, if your Application.cfc and `/resources/` folder aren't in the web-root (e.g. they're inside something like `/api/`) then you'll need to add an [application-specific mapping](http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=appFramework_04.html) for `/resources` so that Taffy can find your resources to initialize the routes.
```js
this.mappings["/resources"] = expandPath("./resources");
```
You'll also need to add a mapping for `/taffy` so that the resources can extend `taffy.core.resource` (since the taffy folder isn't a child of the resources folder):
```js
this.mappings["/taffy"] = expandPath("./taffy");
```
## Authentication and Security
If your authentication and security requirements are simple, you may find the combination of SSL and HTTP Basic Auth to be sufficient. I would advise you not to use Basic Auth without SSL, as it is then easily sniffed. If you're utilizing Basic Auth, Taffy 1.3 added [getBasicAuthCredentials()](#getbasicauthcredentials).
If you're after something a bit more complex, but still short of OAuth, I've written an article covering [Advanced Authentication with Taffy](https://adamtuttle.codes/blog/2013/advanced-authentication-with-taffy-apis/).
Implementing OAuth is something I would like to document, but it is a fairly large topic and I haven't had the time yet. Pull requests welcome.
## Symlink Idiosyncrasies
Adobe ColdFusion (and possibly other engines) historically have been horrible about symlinks inside the web root; and will probably continue to be horrible about them in the future. It's best just to avoid them. They'll cause issues with more than just Taffy.
## Tomcat, JBoss (and other app server) Idiosyncrasies
### 404 when your API is in a subdirectory
It's a known issue, and one we have no control over via code, that **on vanilla Tomcat** (as opposed to the modified version included in Adobe CF10+) **you'll probably find that you get a 404 error if you try to move your api into a subdirectory**. The 404 will complain that the URI is not found, such as:
```
/api/index.cfm/myResource
```
... Of course this is not found, because "index.cfm/myResource" isn't a file.
All hope is not lost, however!
**The Fix:** In your web.xml, you need to add an additional servlet mapping:
```xml
<servlet-mapping>
<servlet-name>CFMLServlet</servlet-name>
<url-pattern>/api/index.cfm/*</url-pattern>
</servlet-mapping>
```
This is because Tomcat doesn't support the use of _two_ wildcards in its mappings. You'll notice that installing ACF or Railo in Tomcat you'll get a web.xml with mappings that have a `url-pattern` of `index.cfm/*`, but unfortunately because of this limitation, you can't change that to `*/index.cfm/*`.
In the xml above you can see that I only have 1 wildcard, but to compensate I've specified the entire path to index.cfm, so that only 1 is needed. If you need to have multiple API's, you'll need a mapping for each index.cfm, and specify the full path of each. (Note that I used `/api/index.cfm` because it matched my example of a 404 for `/api/index.cfm/myResource`... yours should match the location of your index.cfm)
## Railo Idiosyncrasies
### Custom HTTP Status Messages
Taffy allows for setting the HTTP status message using [.withStatus()](#withstatus), such as:
```js
return representationOf({...}).withStatus(403, "Not Authorized");
```
Railo's default setup on Tomcat doesn't allow changing the Status Text header value; so you might use `.withStatus(403, "Account Past Due")` but Tomcat changes this back to `403 Not Authorized`.
You can either work around this by passing status information in the response body, or alternately changing **catalina.properties** (Tomcat config) to include the line:
```conf
org.apache.coyote.USE_CUSTOM_STATUS_MSG_IN_HEADER=true
```
[Hat tip to Jean-Bernard van Zuylen](https://github.com/atuttle/Taffy/issues/121#issuecomment-23410624)
### PUT requests & FORM scope
Prior to versions 4.1.1.002 and 4.2 of Railo, a PUT request would populate the FORM scope. This probably doesn't cause any issues with API's, but did cause a pair of unit tests to fail. (put_body_is_mime_content, and put_body_is_url_encoded_params) This is safe to ignore. Also, upgrade, man!
[Hat tip to Jean-Bernard van Zuylen](https://github.com/atuttle/Taffy/issues/121#issuecomment-23410624)
## More Guides
Some guides are too broad for this document. For your benefit, they are linked here:
- [Migration Guide](https://github.com/atuttle/Taffy/wiki/Migration-Guide) - Notes on upgrading from 1.3.x to 2.x.x
- [Create a dead-simple CRUD API](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Create-a-dead-simple-CRUD-API) walks you through creating your first Taffy-powered API
- [Authentication and Security](https://github.com/atuttle/Taffy/wiki/Authentication-and-Security)
- [Bean Factories](https://github.com/atuttle/Taffy/wiki/Bean-Factories)
- [Common API HTTP Status Codes](https://github.com/atuttle/Taffy/wiki/Common-API-HTTP-Status-Codes)
- [Custom Token Regular Expressions](https://github.com/atuttle/Taffy/wiki/Custom-token-regular-expressions)
- [Exception Logging Adapters](https://github.com/atuttle/Taffy/wiki/Exception-Logging-Adapters) describes integration with BugLogHQ and Hoth, and options for emailing yourself all exceptions
- [Organizing your resources into subfolders](https://github.com/atuttle/Taffy/wiki/Organizing-your-resources-into-subfolders) explains the caveats of using subfolders within /resources
- [URL Rewrite Rule Examples](https://github.com/atuttle/Taffy/wiki/URL-Rewrite-Rule-Examples)
- [Using a Custom Representation Class](https://github.com/atuttle/Taffy/wiki/Using-a-Custom-Representation-Class)
- [Rate Limiting your API](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Rate-Limit-access-to-your-API)
- [Require an API Key](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Require-an-API-Key)
- [Serialize data to a different data type](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Serialize-data-to-a-different-data-type)
- [Share Application variables with another application](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Share-application-variables-with-your-consumer-facing-application)
- [Support returning multiple data formats](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Support-returning-multiple-formats)
- [Allow file uploads to your API](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Upload-a-file-via-your-API)
- [Use an external bean factory to completely manage resources](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-use-an-external-bean-factory-like-coldspring-to-completely-manage-resources)
- [Use an external bean factory to resolve dependencies of your resources](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Use-an-external-bean-factory-like-ColdSpring-to-resolve-dependencies-of-your-resources)
- [Use ColdSpring AOP Advice for your resources](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Use-ColdSpring-AOP-Advice-for-your-resources)
- [Use JSONUtil instead of native JSON serialization](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Use-JSONUtil-instead-of-Native-JSON-serialization)
- [Use the autowire functionality of Taffy's factory to manage resource dependencies](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Use-Taffy's-built-in-Dependency-Injection-to-resolve-dependencies-of-your-resources)
- [Write your components using CF9 script-component syntax](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Write-your-components-using-CF9--script-component-syntax)
## Configuration Reference
### Configuration via Metadata
Metadata is used to apply configuration in a concise and elegant manor, where necessity and possibility intersect.
#### In Resources
Resources are CFCs that interpret a request and provide or update the requested data.
##### taffy:uri
The **taffy:uri** property applies to the `<cfcomponent>` tag or the `component{}` definition. It configures which URIs the CFC will respond to. There is no limit to the number of tokens that can be used in a single URI, as long as they are not consecutive and are delimited by at least one character (like a slash). Examples:
```xml
<cfcomponent taffy:uri="/artist/{artistId}">
</cfcomponent>
```
or
```js
component taffy_uri="/artist/{artistId}" {
}
```
The following example is _invalid_, because the tokens are not separated by (at least) a slash:<br/>
```
taffy:uri="/artist/{artistId}{artistName}"
```
##### Tokens
Tokens in URIs define the parts of the URI that are dynamic. They are identified by curly-brackets: `{tokenName}` and are passed **by name** to all functions that handle the request. The `{foo}` token's value will be passed to the `foo` argument of the associated method.
##### URI Matching Order
As of Taffy 1.3, URIs are processed into a sorted array on API startup, and searched in order for every request, such that `/artists/list` will match before `/artists/{id}`. You should design your API URI's accordingly. Pay special attention to placement and possible values for URI tokens. If a possible value is the same as a static URI, consider changing URI formatting. For example, if we had an artist with id "list", then having the static URI `/artists/list` would prevent the `/artists/{id}` URI from ever matching when the intention is to get the individual artist record for artist with id "list."
##### taffy:verb
By convention, resources will automatically map the 4 primary HTTP REST verbs -- GET, PUT, POST, DELETE -- to CFC methods with the same name. For extended verbs -- OPTIONS, HEAD -- or if you want to map one of the primary verbs to a method with a different name, you can use the `taffy:verb` metadata property on the method to specify the verb it should respond to. Examples:
```xml
<cffunction name="getUser" taffy:verb="get">
<cfargument name="userId" type="numeric" />
</cffunction>
```
or
```js
function getUser( numeric userId ) taffy_verb="get" {
}
```
#### In Representation Classes
Representation classes are used to take the data provided by a resource and serialize it into a format usable by the web service consumer. A single Representation Class is capable of serializing native data objects (strings, numbers, queries, structures, arrays, etc) into 1 or more formats. Typical formats include JSON, XML, or YAML, but are not limited.
##### taffy:mime
By convention, the mime-types supported by your API are determined by the method names in your default representation class. (The included default representation class supports only JSON.) Each `getAsX` method in your representation class describes a new mime type, defined in two parts: The X portion of the method name determines the extension of the mime type -- which can be appended to the URI as if it were a file, as in: `/artists/42.json` which would use `getAsJson`. Taffy will also return a content-type header with the content type that you supply in the `taffy:mime` metadata property of the `getAsX` method. Typically, this is something like "application/json" or "application/xml". Examples:
```xml
<cffunction name="getAsJson" taffy:mime="application/json">
</cffunction>
```
or
```js
function getAsJson() taffy_mime="application/json" {
}
```
##### taffy:default
When your API supports more than one data format (i.e. json and xml), you must set one as the default. You do this with the `taffy:default` property, which expects a boolean value of either TRUE or FALSE. The default is FALSE, and you never need to include `taffy:default="false"`. You only need to include `taffy:default="true"` on one method -- the one that should be the default. Examples:
```xml
<cffunction name="getAsJson" taffy:mime="application/json" taffy:default="true">
</cffunction>
<cffunction name="getAsXml" taffy:mime="application/xml">
</cffunction>
```
or
```js
function getAsJson() taffy_mime="application/json" taffy_default="true" {}
function getAsXml() taffy_mime="application/xml" {}
```
### variables.framework settings
Default values:
```js
variables.framework = {
reloadKey = "reload",
reloadPassword = "true",
reloadOnEveryRequest = false,
endpointURLParam = "endpoint",
representationClass = "taffy.core.nativeJsonRepresentation",
dashboardKey = "dashboard",
disableDashboard = false,
disabledDashboardRedirect = "",
jsonp = false,
unhandledPaths = "/flex2gateway",
allowCrossDomain = false,
globalHeaders = structNew(),
debugKey = "debug",
useEtags = false,
returnExceptionsAsJson = true,
exceptionLogAdapter = "taffy.bonus.LogToScreen",
exceptionLogAdapterConfig = {},
beanFactory = "",
environments = {}
};
```
#### reloadKey
**Available in:** Taffy 1.2+<br/>
**Type:** String<br/>
**Default:** "reload"<br/>
**Description:** Name of the url parameter that requests the framework to be reloaded. Used in combination with the reload password (see: reloadPassword), the framework will re-initialize itself. During re-initialization, all configuration settings are re-applied and all cached objects are cleared and reloaded. If the value of the key does not match the reload password, a reload will not be performed. This allows you to set a secret password to restrict control of reloading your API to trusted parties.
#### reloadPassword
**Available in:** Taffy 1.2+<br/>
**Type:** String<br/>
**Default:** "true"<br/>
**Description:** Accepted value of the url parameter that requests the framework to be reloaded. Used in combination with the reload key (see: reloadKey), the framework will re-initialize itself. During re-initialization, all configuration settings are re-applied and all cached objects are cleared and reloaded. If the value of the key does not match the reload password, a reload will not be performed. This allows you to set a secret password to restrict control of reloading your API to trusted parties.
#### reloadOnEveryRequest
**Available in:** Taffy 1.2+<br/>
**Type:** Boolean<br/>
**Default:** False<br/>
**Description:** Flag that indicates whether Taffy should reload cached values and configuration on every request. Useful in development; set to FALSE in production.
#### endpointURLParam
**Available in:** Taffy 1.3+<br/>
**Type:** String<br/>
**Default:** "endpoint"<br/>
**Description:** The query-string parameter name that can optionally be used to specify URI. Until now, URI formatting has been required to be index.cfm/URI; this parameter allows you to use index.cfm?endpoint=/URI. This setting (endpointURLParam) allows you to change the default parameter name of "endpoint" to something custom.
#### representationClass
**Available in:** Taffy 1.2+<br/>
**Type:** String<br/>
**Default:** "taffy.core.nativeJsonRepresentation"<br/>
**Description:** The CFC dot-notation path, or bean name, of the representation class that your API will use to serialize returned data for the client.
#### dashboardKey
**Available in:** Taffy 1.2+<br/>
**Deprecated **in: Taffy 1.3+<br/>
**Type:** String<br/>
**Default:** "dashboard"
Description: Name of the url parameter that displays the dashboard. The dashboard displays resources that your API is aware of, generates documentation about your API based on hint attributes, and contains a mock client to make testing your API easy.
#### disableDashboard
**Available in:** Taffy 1.2+<br/>
**Type:** Boolean<br/>
**Default:** False<br/>
**Description:** Whether or not Taffy will allow the dashboard to be displayed. If set to true, the dashboard key is simply ignored. You may wish to disable the dashboard in production, depending on whether or not you want customers/clients to be able to see it.
#### disabledDashboardRedirect
**Available in:** Taffy 1.3+<br/>
**Type:** String<br/>
**Default:** ""<br/>
**Description:** URL to which Taffy should redirect (302) the client/browser if the dashboard is disabled. If the dashboard is disabled and this value is blank, a simple 403 Forbidden response is sent instead.
#### jsonp
**Available in:** Taffy 2.0+<br/>
**Type:** Boolean/String<br/>
**Default:** false<br/>
**Description:** When false, JSONP is disabled. To enable it, change the value to a string, such as "callback". The value you specify will be the query parameter in which Taffy expects to find the JSONP callback name. Note: JSONP only works for GET requests (by design!)
#### unhandledPaths
**Available in:** Taffy 1.2+<br/>
**Type:** String (Comma-delimited list)<br/>
**Default:** "/flex2gateway"<br/>
**Description:** Set a list of paths (usually subfolders of the API) that you do not want Taffy to interfere with. Unless listed here, Taffy takes over the request lifecycle and does not execute the requested ColdFusion template.
#### allowCrossDomain
**Available in:** Taffy 1.2+<br/>
**Type:** Boolean<br/>
**Default:** False<br/>
**Description:** Whether or not to allow cross-domain access to your API. Turning this on adds the following headers:
```xml
<cfheader name="Access-Control-Allow-Origin" value="*" />
<cfheader name="Access-Control-Allow-Methods" value="#allowedVerbs#" />
<cfheader name="Access-Control-Allow-Headers" value="Content-Type" />
```
The allowed verbs, of course, are the ones allowed by the requested resource, as well as OPTIONS.
#### globalHeaders
**Available in:** Taffy 1.2+<br/>
**Type:** Structure<br/>
**Default:** {}<br/>
**Description:** A structure where each key is the name of a header you want to return, such as "X-MY-HEADER" and the structure value is the header value.
Global headers are static. You set them on application initialization and they do not change. If you need dynamic headers, you can add them to each response at runtime using withHeaders().
#### debugKey
**Available in:** Taffy 1.2+<br/>
**Type:** String<br/>
**Default:** "debug"<br/>
**Description:** Name of the url parameter that enables CF Debug Output.
#### useEtags
**Available in:** Taffy 1.3+<br/>
**Type:** Boolean<br/>
**Default:** False<br/>
**Description:** Enable the use of HTTP ETags for caching purposes. Taffy will automatically handle both sending the server ETag value and detecting client supplied ETags (via the If-None-Match header) for you; simply turn this setting on.
**NOTE FOR RAILO USERS:** While it will not cause errors, the underlying Java code used in this feature was improperly implemented prior to Railo 4.0.? and this could result in your result data being sent as if it were changed when it in fact has not. (I'm not sure which Railo point release will include the fix. The latest as of this writing is version 4.0.2, and does not include it.) Adobe ColdFusion is unaffected.
#### returnExceptionsAsJson
**Available in:** Taffy 1.2+<br/>
**Type:** Boolean<br/>
**Default:** true<br/>
**Description:** When an error occurs that is not otherwise handled, this option tells Taffy to attempt to format the error information as JSON and return that (regardless of the requested return format). As of Taffy 2.1 this also includes a structured stack trace with file names and line numbers.
#### exceptionLogAdapter
**Available in:** Taffy 1.2+<br/>
**Type:** String<br/>
**Default:** "taffy.bonus.LogToEmail"<br/>
**Description:** CFC dot-notation path to the exception logging adapter you want to use. Default adapter simply emails all exceptions. See [Exception Logging Adapters](https://github.com/atuttle/Taffy/wiki/Exception-Logging-Adapters) for more details.
#### exceptionLogAdapterConfig
**Available in:** Taffy 1.2+<br/>
**Type:** Any<br/>
**Default:** See defaults [here](#variablesframework-settings)<br/>
**Description:** Configuration that your chosen logging adapter requires. Can be any data type. See [Exception Logging Adapters](https://github.com/atuttle/Taffy/wiki/Exception-Logging-Adapters) for more details.
#### beanFactory
**Available in:** Taffy 1.2+<br/>
**Type:** Object Instance<br/>
**Default:** ""<br/>
**Description:** Already instantiated and cached (e.g. in Application scope) object instance of your external bean factory. Not required in order to use Taffy's built-in factory.
#### environments
**Available in:** Taffy 1.3+<br/>
**Type:** Structure<br/>
**Default:** {}<br/>
**Description:** Environment-specific overrides to any framework settings. Applied after general variables.framework settings, and after configureTaffy() has been called. See [getEnvironment()](#getenvironment) for more details.
## Index of API Methods
### Application.cfc
#### getPath()
**Parameters:** _(none)_
This method is provided as an extension point. On Adobe ColdFusion ("ACF") 9, installed in standard entire-server mode, no change should be necessary. However, if ACF is installed on another JEE app server (i.e. Tomcat, Glassfish, etc), or on JRun but using an EAR/WAR setup, then you may need to override this method to make Taffy work on your server.
#### getBasicAuthCredentials()
**Parameters:** _(none)_
**Added in Taffy 1.3.** This method returns a structure with two keys: `username`, and `password`. When the client does not provide HTTP Basic Auth credentials, the username and password keys will be blank. When the client does provide them, the values will be available in these keys.
This method is only available inside your Application.cfc. If, for example, you need access to the username in a resource, you can use this method inside onTaffyRequest and add the username to the requestArguments structure.
#### getBeanFactory()
**Parameters:** _(none)_
Returns whatever bean factory you may have set into Taffy, if any. If using the `/resources` folder, it returns Taffy's built-in factory.
#### newRepresentation()
**Use it inside:** onTaffyRequest<br/>
**Parameters:**
- **class (string)** - the dot-notation cfc path (or bean id if managing the class with a bean factory, including the built-in factory) for the representation class. Optional. Default value is the defined representation class. If you're using the framework default representation class, it's `nativeJsonRepresentation`.
Use this method inside onTaffyRequest when you want to _abort_ the request with some specific message or data. Use the `setData` method on the returned representation class to put your data/message into it before returning it.
#### getEnvironment()
**Parameters:** _(none)_
Taffy calls this method during initialization to determine in which configured environment, if any, it is executing. Overriding it is optional. Use whatever methodology you like to determine the result (e.g. hostname, reading a file or registry setting, etc.), and simply return, as string, the name of the current environment.
The returned value will be used to load environment-specific configuration. For example, if you have the following code in your Application.cfc, then the dashboard will be disabled in production:
```js
variables.framework = {
disableDashboard = false
,environments = {
production = {
disableDashboard = true
}
}
};
function getEnvironment(){
if (...){
return "production";
}else{
return "development";
}
}
```
#### onTaffyRequest()
**Parameters:**
- **verb (string)** - The HTTP request verb provided by the client
- **cfc (string)** - The CFC name (minus ".cfc") that would handle the request. (Bean Name, if using an external bean factory.)
- **requestArguments (struct)** - A structure containing all of the arguments of the request, including tokens from the URI as well as any query string parameters.
- **mimeExt (string)** - The mime extension (e.g. "json" - NOT the full mime type, e.g. "application/json")
- **headers (struct)** - A structure containing each header from the request, as sent by the client.
- **methodMetadata (struct)** - A structure containing any non-taffy metadata set on the requested resource method.
This method is optional, and allows you to inspect and potentially abort an API request in a way that adheres to the HTTP specification and REST best practices. If you choose not to override it (by implementing it in your Application.cfc), it will always return true, allowing the request to continue. If you implement it, you can check for things like an API key, or whether or not the customer has paid for your service, and return something other than the data that they are requesting.
If you do not return TRUE, allowing the request to continue as normal, then Taffy expects you to return a **[representation](#custom-representation-classes)** (either the default class, or a custom one) that it should immediately return to the consumer, serialized to the appropriate format. If you simply want to return with a status code of 403 (which indicates "Not Allowed"), you could do this:
```js
return newRepresentation().noData().withStatus(403);
```
Alternately, you could return some data to indicate that they owe you money or something:
```js
return newRepresentation()
.setData({error="Your account is past due. Please email accounts payable."})
.withStatus(403);
```
The options here are limited only by your imagination.
You can add data to the **requestArguments** structure and this will be passed on to any resource that handles the request. Simply add a key to the structure:
```js
function onTaffyRequest(verb, cfc, requestArguments, mimeExt, headers) {
arguments.requestArguments.myData = "myvalue";
return true;
}
```
In your resource:
```js
function get(myData) {
//arguments.myData => "myValue"
}
```
You can use the method metadata for anything you see fit; but the original use case was for role-based security. Consider the following resource method:
```js
public function getData( id ) taffy_method="get" role="datareader" { ... }
```
Taffy doesn't do anything with the **role** metadata on this method other than expose it to you in onTaffyRequest. So let's use the user's API key to find out what roles they have, and verify that the method's required role is among them. This is a snippet from your Application.cfc:
```js
function onTaffyRequest(verb, cfc, requestArgs, mime, head, methodMetadata){
local.user = (...); //get user from api key...
if (structKeyExists(methodMetadata, "role")){
for (var availableRole in local.user.roles){
if (availableRole == methodMetadata.role) { return true; }
}
return newRepresentation().noData().withStatus(403, "Not Authorized");
}else{
//no role required on the method, so allow anyone to use it
return true;
}
}
```
Here, if the user doesn't have the `datareader` role assigned, they'll get a 403 response.
### Resources
Resource CFCs extend `taffy.core.resource`. The following methods are available inside each of your Resource CFCs:
#### noData()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:** _(none)_
This method allows you to specify that there is no data to be returned for the current request. Generally, you would use it in conjunction with the **withStatus** method to set a specific return status for the request. For example, if the requested resource doesn't exist, you could return a 404 error like so:
```js
return noData().withStatus(404);
```
#### queryToArray()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **data (query)** - The query object to be transformed
This method transforms a ColdFusion query object into an array of structures. It was added because ColdFusion's serializeJSON functionality uses an ..._eccentric_... format for queries. **queryToArray** returns the format most people expect: a vanilla array of structures with named keys. To be fair the ACF serialization format uses less data as long as there is more than 1 row in the query, but it doesn't matter that you do a better job if nobody understands your output. _queryToArray also preserves query column name case, which serializeJSON does not._
#### queryToStruct()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **data (query)** - The query object to be transformed
This method transforms a ColdFusion query object into a structure. If there is more than one row in the query, only the first row is used. This is a sort of shorthand syntax for `queryToArray(qry)[1]`, when you know there will only be one record and you want the structure, but not wrapped in an array. Like **queryToArray**, **queryToStruct** preserves query column casing in the structure key names.
#### representationOf()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **data (any)** - The data to return to the consumer.
Data can be of any type, including complex data types like queries, structures, and arrays, as long as the serializer knows how to serialize them. For more information on using a custom representation class, see [Using a custom Representation Class](https://github.com/atuttle/Taffy/wiki/Using-a-Custom-Representation-Class).
#### saveLog()
**Use it inside:** anywhere inside a Resource CFC to log data using your [configured logging adapter](https://github.com/atuttle/Taffy/wiki/Exception-Logging-Adapters).<br/>
**Parameters:**
- **exception (struct)** - traditionally a CF exception object, but any struct may be passed.
What you pass to this method is simply handed off to the logging adapter. You may use one of the included adapters (LogToEmail, LogToBuglogHQ, or LogToHoth), or a custom logging adapter. If you write a custom logging adapter, it should implement the `taffy.bonus.ILogAdapter` interface.
If you don't configure a logging adapter, the default is LogToEmail, but the default `from` and `to` email addresses are not useful. See [Exception Log Adapters](https://github.com/atuttle/Taffy/wiki/Exception-Logging-Adapters) for more information on configuring logging adapters.
#### streamBinary()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **binaryData (any)** - the binary data to be streamed back to the consumer
Use this method in place of `representationOf()` to return a stream of binary data. Useful for streaming things like dynamically generated PDFs.
#### streamFile()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **fileName (string)** - fully qualified file path (eg c:\tmp\files.zip)
Use this method in place of `representationOf()` to stream a file from disk (or VFS). Optionally append `.andDelete( true )` to delete the file once streaming is complete.
```js
return streamFile("/foo.txt").andDelete(true);
```
#### streamImage()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **binaryData (any)** - the binary, base64 encoded data of the image, to be streamed back to the consumer
Use this method in place of `representationOf()` to stream an image from disk (or VFS).
#### withHeaders()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **headerStruct (struct)** - A structure whose keys are desired header names ("x-powered-by") and whose associated values are the values for the corresponding headers ("Taffy 1.1!").
This special method _**requires**_ the use of either **noData** or **representationOf**. It adds custom headers to the return. Additional use of **withStatus** optional.
```js
return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"});
```
#### withMime()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **mime (string)** - mime type (eg. "application/pdf") to be returned with the streamed file data
This special method _**requires**_ the use of either **streamFile** or **streamBinary**. It overrides the default mime type header for the return.
```js
return streamFile("kittens/cuteness.pdf").withMime("application/pdf");
```
#### withStatus()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **statusCode (numeric)** - the [HTTP Status Code](http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html) to return to the consumer.
- **statusText (string)** - the Status Text to include with the HTTP Status Code. The "OK" part of "200 OK"; the "Not Found" part of "404 Not Found".
This special method _**requires**_ the use of either **noData** or **representationOf**. It sets the HTTP Status Code of the return. Additional use of **withHeaders** optional.
_If you do not specify a return status code, Taffy will always return status code 200 (OK) by default._
```js
return noData().withStatus(404, "Not Found");
```
### Custom Representation Classes
Representation Classes have the job of converting the native data that your resources return (queries, structures, arrays, and so on...) into &mdash;usually&mdash; a string that can be sent to the client and used there. It's also possible to send images and binary files like PDFs and ZIPs, but that's a topic that has its own guide. Taffy comes with a representation class that uses ColdFusion's **serializeJson** method to convert native data to JSON strings, and uses this class by default.
_Why would you want to create a custom representation class, and how would you do it?_
#### Why
You want to create a Custom Representation Class ("CRC") if you're stymied by [the bugs in ColdFusion's serializeJson method](http://fusiongrokker.com/post/you-can-help-fix-coldfusion-json-serialization), or if you want to return something other than JSON: XML for example. Another reason to use a CRC is when your api should be capable of returning more than one return format &mdash; e.g. both XML and JSON.
#### How
A Custom Representation Class is a CFC that:
- Extends `taffy.core.baseRepresentation`
- Implements 1 or more "getAs{something}" methods
- ... that defines a `taffy:mime` attribute
Let's take a look at **taffy.core.nativeJsonRepresentation** &mdash; the default representation class that Taffy uses unless you configure something else.
```xml
<cfcomponent extends="taffy.core.baseRepresentation" output="false">
<cffunction
name="getAsJson"
output="false"
taffy:mime="application/json"
taffy:default="true"
hint="serializes data as JSON">
<cfreturn serializeJSON(variables.data) />
</cffunction>
</cfcomponent>
```
- Extends `taffy.core.baseRepresentation`? Check.
- Implements 1 or more "getAs{something}" methods? Check.
- ... that defines a `taffy:mime` attribute? Check.
The base class is easy to explain: It makes everything about CRC's work without you having to do anything. It's the black magic.
What's the `getAs{something}` method for? Well, it's for **get**ting the data out of your CRC in a format that the user is requesting. The client can specify its desired return format in one of two ways. An HTTP `Accept` header, or by appending `.{format}` to the URL.
The former looks like: `Accept: application/json`, and the latter looks like:
```
http://example.com/api/v1/index.cfm/users.json
```
You may be familiar or comfortable with either, but the point is that they both mean the same thing. Taffy looks at the methods in your CRC at startup to determine what types your API will support. It needs both the format-extension (.json, via `getAsJson`) and the mime type (application/json, via taffy:mime) in order to function properly.
When writing your own CRC, you can assume that a resource has returned data and the returned value is available to the CRC as `variables.data`. This is why the default class returns `serializeJson(variables.data)`. If your method is `getAsXML` then it needs to be able to convert the data into an XML string.
#### Multiple Formats
Since a common use-case for CRCs is supporting multiple data formats, we'll next look at **CustomRepresentationClass.cfc**, found in Taffy's **examples/api_twoFormats/resources** folder.
```xml
<cfcomponent extends="taffy.core.baseRepresentation">
<cfset variables.jsonUtil = application.jsonUtil />
<cfset variables.AnythingToXML = application.AnythingToXML />
<cffunction name="getAsJSON" taffy:mime="application/json" taffy:default="true" output="false">
<cfreturn variables.jsonUtil.serializeJson(variables.data) />
</cffunction>
<cffunction name="getAsXML" taffy:mime="application/xml" output="false">
<cfreturn variables.AnythingToXML.ToXML(variables.data) />
</cffunction>
</cfcomponent>
```
Here we see two getters: `getAsJSON` with `taffy:mime="application/json"` and `getAsXML` with `taffy:mime="application/xml"`. Each is using a 3rd party library (available in the example folder) to do the serialization work. It's assuming that these libraries are available in Application scope (check Application.cfc for the implementation). This is generally regarded as a bad practice ("tight coupling"), and Taffy does not require that your code be structured this way. It's only included this way to keep the example as simple and focused as possible.
When a request is made that wants XML back, Taffy calls the `getAsXML` method to get the XML string. When a request is made for JSON data, Taffy calls `getAsJSON`.
And that &mdash;in a nutshell&mdash; is Custom Representation Classes.
+807
View File
@@ -0,0 +1,807 @@
# Taffy
The REST Web Service framework for ColdFusion and Railo
<a id="download" href="https://github.com/atuttle/Taffy/archive/v2.2.4.zip">Download Taffy 2.2.4.zip</a>
- [Release Notes](https://github.com/atuttle/Taffy/releases/tag/v2.2.4)
- [GitHub](https://github.com/atuttle/Taffy)
- Join us in the **#taffy** chat room on the [CFML Slack](https://cfml-slack.herokuapp.com/)
## Quickstart: Your First API
**Application.cfc:**
```js
component extends="taffy.core.api" {}
```
**index.cfm:**
```html
<!-- this space left intentionally blank -->
```
**/resources/hello.cfc:**
```js
component extends="taffy.core.resource" taffy_uri="/hello" {
function get(){
return representationOf(['hello','world']);
}
}
```
Congratulations, you've just written your first API. Didn't think it could be so simple, did you? Indeed, [you can fit an entire Taffy API into a single tweet](https://twitter.com/cf_taffy/statuses/327415972581486592).
Point your browser to the empty **index.cfm** file you created, and have a look at the dashboard. Click the "hello" row to expand it, and click the **Send** button to make a REST request. The results of your request appear below the request parameters.
The response includes a **status code** and **status text**, as well as other **headers**, and the **response body** (if applicable). Taffy also shows you the amount of time the request took.
### Getting Started, with more details
Here's a [Getting Started guide](https://github.com/atuttle/Taffy/wiki/Getting-Started) that goes into a little bit more detail, if the above example wasn't enough.
## Installation Options
### Global /taffy Mapping
Best practice would be to keep Taffy _out of your web root_. This is also (almost) the easiest install method. Instead of putting taffy in the web root, use a global mapping (set in the CF Administrator) pointing `/taffy` to wherever you have it saved.
### /taffy in Web Root
Just put the /taffy folder at the root of your domain. Boom, done.
### Sub-folder
If you aren't allowed to create a global mapping, or for some other reason want to, you can install Taffy as a sub-folder of your API. (NB: This would also allow you to use multiple versions of Taffy on the same CF Instance.)
Using sub-folders requires the use of Application-Specific Mappings (introduced in Adobe ColdFusion 8). Start by creating this directory structure:
```
/path-to-your-api
├── Application.cfc
├── index.cfm
├─┬ /taffy
│ ├── /bonus
│ ├── /core
│ └── /dashboard
└─┬ /resources
├── ...
└── someResource.cfc
```
You can see that the taffy folder is a sibling to Application.cfc. This allows Application.cfc to use relative paths to extend `taffy.core.api`.
Next, if your Application.cfc and `/resources/` folder aren't in the web-root (e.g. they're inside something like `/api/`) then you'll need to add an [application-specific mapping](http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=appFramework_04.html) for `/resources` so that Taffy can find your resources to initialize the routes.
```js
this.mappings["/resources"] = expandPath("./resources");
```
You'll also need to add a mapping for `/taffy` so that the resources can extend `taffy.core.resource` (since the taffy folder isn't a child of the resources folder):
```js
this.mappings["/taffy"] = expandPath("./taffy");
```
## Authentication and Security
If your authentication and security requirements are simple, you may find the combination of SSL and HTTP Basic Auth to be sufficient. I would advise you not to use Basic Auth without SSL, as it is then easily sniffed. If you're utilizing Basic Auth, Taffy 1.3 added [getBasicAuthCredentials()](#getbasicauthcredentials).
If you're after something a bit more complex, but still short of OAuth, I've written an article covering [Advanced Authentication with Taffy](https://adamtuttle.codes/blog/2013/advanced-authentication-with-taffy-apis/).
Implementing OAuth is something I would like to document, but it is a fairly large topic and I haven't had the time yet. Pull requests welcome.
## Symlink Idiosyncrasies
Adobe ColdFusion (and possibly other engines) historically have been horrible about symlinks inside the web root; and will probably continue to be horrible about them in the future. It's best just to avoid them. They'll cause issues with more than just Taffy.
## Tomcat, JBoss (and other app server) Idiosyncrasies
### 404 when your API is in a subdirectory
It's a known issue, and one we have no control over via code, that **on vanilla Tomcat** (as opposed to the modified version included in Adobe CF10+) **you'll probably find that you get a 404 error if you try to move your api into a subdirectory**. The 404 will complain that the URI is not found, such as:
```
/api/index.cfm/myResource
```
... Of course this is not found, because "index.cfm/myResource" isn't a file.
All hope is not lost, however!
**The Fix:** In your web.xml, you need to add an additional servlet mapping:
```xml
<servlet-mapping>
<servlet-name>CFMLServlet</servlet-name>
<url-pattern>/api/index.cfm/*</url-pattern>
</servlet-mapping>
```
This is because Tomcat doesn't support the use of _two_ wildcards in its mappings. You'll notice that installing ACF or Railo in Tomcat you'll get a web.xml with mappings that have a `url-pattern` of `index.cfm/*`, but unfortunately because of this limitation, you can't change that to `*/index.cfm/*`.
In the xml above you can see that I only have 1 wildcard, but to compensate I've specified the entire path to index.cfm, so that only 1 is needed. If you need to have multiple API's, you'll need a mapping for each index.cfm, and specify the full path of each. (Note that I used `/api/index.cfm` because it matched my example of a 404 for `/api/index.cfm/myResource`... yours should match the location of your index.cfm)
## Railo Idiosyncrasies
### Custom HTTP Status Messages
Taffy allows for setting the HTTP status message using [.withStatus()](#withstatus), such as:
```js
return representationOf({...}).withStatus(403, "Not Authorized");
```
Railo's default setup on Tomcat doesn't allow changing the Status Text header value; so you might use `.withStatus(403, "Account Past Due")` but Tomcat changes this back to `403 Not Authorized`.
You can either work around this by passing status information in the response body, or alternately changing **catalina.properties** (Tomcat config) to include the line:
```conf
org.apache.coyote.USE_CUSTOM_STATUS_MSG_IN_HEADER=true
```
[Hat tip to Jean-Bernard van Zuylen](https://github.com/atuttle/Taffy/issues/121#issuecomment-23410624)
### PUT requests & FORM scope
Prior to versions 4.1.1.002 and 4.2 of Railo, a PUT request would populate the FORM scope. This probably doesn't cause any issues with API's, but did cause a pair of unit tests to fail. (put_body_is_mime_content, and put_body_is_url_encoded_params) This is safe to ignore. Also, upgrade, man!
[Hat tip to Jean-Bernard van Zuylen](https://github.com/atuttle/Taffy/issues/121#issuecomment-23410624)
## More Guides
Some guides are too broad for this document. For your benefit, they are linked here:
- [Migration Guide](https://github.com/atuttle/Taffy/wiki/Migration-Guide) - Notes on upgrading from 1.3.x to 2.x.x
- [Create a dead-simple CRUD API](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Create-a-dead-simple-CRUD-API) walks you through creating your first Taffy-powered API
- [Authentication and Security](https://github.com/atuttle/Taffy/wiki/Authentication-and-Security)
- [Bean Factories](https://github.com/atuttle/Taffy/wiki/Bean-Factories)
- [Common API HTTP Status Codes](https://github.com/atuttle/Taffy/wiki/Common-API-HTTP-Status-Codes)
- [Custom Token Regular Expressions](https://github.com/atuttle/Taffy/wiki/Custom-token-regular-expressions)
- [Exception Logging Adapters](https://github.com/atuttle/Taffy/wiki/Exception-Logging-Adapters) describes integration with BugLogHQ and Hoth, and options for emailing yourself all exceptions
- [Organizing your resources into subfolders](https://github.com/atuttle/Taffy/wiki/Organizing-your-resources-into-subfolders) explains the caveats of using subfolders within /resources
- [URL Rewrite Rule Examples](https://github.com/atuttle/Taffy/wiki/URL-Rewrite-Rule-Examples)
- [Using a Custom Representation Class](https://github.com/atuttle/Taffy/wiki/Using-a-Custom-Representation-Class)
- [Rate Limiting your API](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Rate-Limit-access-to-your-API)
- [Require an API Key](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Require-an-API-Key)
- [Serialize data to a different data type](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Serialize-data-to-a-different-data-type)
- [Share Application variables with another application](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Share-application-variables-with-your-consumer-facing-application)
- [Support returning multiple data formats](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Support-returning-multiple-formats)
- [Allow file uploads to your API](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Upload-a-file-via-your-API)
- [Use an external bean factory to completely manage resources](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-use-an-external-bean-factory-like-coldspring-to-completely-manage-resources)
- [Use an external bean factory to resolve dependencies of your resources](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Use-an-external-bean-factory-like-ColdSpring-to-resolve-dependencies-of-your-resources)
- [Use ColdSpring AOP Advice for your resources](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Use-ColdSpring-AOP-Advice-for-your-resources)
- [Use JSONUtil instead of native JSON serialization](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Use-JSONUtil-instead-of-Native-JSON-serialization)
- [Use the autowire functionality of Taffy's factory to manage resource dependencies](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Use-Taffy's-built-in-Dependency-Injection-to-resolve-dependencies-of-your-resources)
- [Write your components using CF9 script-component syntax](https://github.com/atuttle/Taffy/wiki/So-you-want-to:-Write-your-components-using-CF9--script-component-syntax)
## Configuration Reference
### Configuration via Metadata
Metadata is used to apply configuration in a concise and elegant manor, where necessity and possibility intersect.
#### In Resources
Resources are CFCs that interpret a request and provide or update the requested data.
##### taffy:uri
The **taffy:uri** property applies to the `<cfcomponent>` tag or the `component{}` definition. It configures which URIs the CFC will respond to. There is no limit to the number of tokens that can be used in a single URI, as long as they are not consecutive and are delimited by at least one character (like a slash). Examples:
```xml
<cfcomponent taffy:uri="/artist/{artistId}">
</cfcomponent>
```
or
```js
component taffy_uri="/artist/{artistId}" {
}
```
The following example is _invalid_, because the tokens are not separated by (at least) a slash:<br/>
```
taffy:uri="/artist/{artistId}{artistName}"
```
##### Tokens
Tokens in URIs define the parts of the URI that are dynamic. They are identified by curly-brackets: `{tokenName}` and are passed **by name** to all functions that handle the request. The `{foo}` token's value will be passed to the `foo` argument of the associated method.
##### URI Matching Order
As of Taffy 1.3, URIs are processed into a sorted array on API startup, and searched in order for every request, such that `/artists/list` will match before `/artists/{id}`. You should design your API URI's accordingly. Pay special attention to placement and possible values for URI tokens. If a possible value is the same as a static URI, consider changing URI formatting. For example, if we had an artist with id "list", then having the static URI `/artists/list` would prevent the `/artists/{id}` URI from ever matching when the intention is to get the individual artist record for artist with id "list."
##### taffy:verb
By convention, resources will automatically map the 4 primary HTTP REST verbs -- GET, PUT, POST, DELETE -- to CFC methods with the same name. For extended verbs -- OPTIONS, HEAD -- or if you want to map one of the primary verbs to a method with a different name, you can use the `taffy:verb` metadata property on the method to specify the verb it should respond to. Examples:
```xml
<cffunction name="getUser" taffy:verb="get">
<cfargument name="userId" type="numeric" />
</cffunction>
```
or
```js
function getUser( numeric userId ) taffy_verb="get" {
}
```
#### In Representation Classes
Representation classes are used to take the data provided by a resource and serialize it into a format usable by the web service consumer. A single Representation Class is capable of serializing native data objects (strings, numbers, queries, structures, arrays, etc) into 1 or more formats. Typical formats include JSON, XML, or YAML, but are not limited.
##### taffy:mime
By convention, the mime-types supported by your API are determined by the method names in your default representation class. (The included default representation class supports only JSON.) Each `getAsX` method in your representation class describes a new mime type, defined in two parts: The X portion of the method name determines the extension of the mime type -- which can be appended to the URI as if it were a file, as in: `/artists/42.json` which would use `getAsJson`. Taffy will also return a content-type header with the content type that you supply in the `taffy:mime` metadata property of the `getAsX` method. Typically, this is something like "application/json" or "application/xml". Examples:
```xml
<cffunction name="getAsJson" taffy:mime="application/json">
</cffunction>
```
or
```js
function getAsJson() taffy_mime="application/json" {
}
```
##### taffy:default
When your API supports more than one data format (i.e. json and xml), you must set one as the default. You do this with the `taffy:default` property, which expects a boolean value of either TRUE or FALSE. The default is FALSE, and you never need to include `taffy:default="false"`. You only need to include `taffy:default="true"` on one method -- the one that should be the default. Examples:
```xml
<cffunction name="getAsJson" taffy:mime="application/json" taffy:default="true">
</cffunction>
<cffunction name="getAsXml" taffy:mime="application/xml">
</cffunction>
```
or
```js
function getAsJson() taffy_mime="application/json" taffy_default="true" {}
function getAsXml() taffy_mime="application/xml" {}
```
### variables.framework settings
Default values:
```js
variables.framework = {
reloadKey = "reload",
reloadPassword = "true",
reloadOnEveryRequest = false,
endpointURLParam = "endpoint",
representationClass = "taffy.core.nativeJsonRepresentation",
dashboardKey = "dashboard",
disableDashboard = false,
disabledDashboardRedirect = "",
jsonp = false,
unhandledPaths = "/flex2gateway",
allowCrossDomain = false,
globalHeaders = structNew(),
debugKey = "debug",
useEtags = false,
returnExceptionsAsJson = true,
exceptionLogAdapter = "taffy.bonus.LogToScreen",
exceptionLogAdapterConfig = {},
beanFactory = "",
environments = {}
};
```
#### reloadKey
**Available in:** Taffy 1.2+<br/>
**Type:** String<br/>
**Default:** "reload"<br/>
**Description:** Name of the url parameter that requests the framework to be reloaded. Used in combination with the reload password (see: reloadPassword), the framework will re-initialize itself. During re-initialization, all configuration settings are re-applied and all cached objects are cleared and reloaded. If the value of the key does not match the reload password, a reload will not be performed. This allows you to set a secret password to restrict control of reloading your API to trusted parties.
#### reloadPassword
**Available in:** Taffy 1.2+<br/>
**Type:** String<br/>
**Default:** "true"<br/>
**Description:** Accepted value of the url parameter that requests the framework to be reloaded. Used in combination with the reload key (see: reloadKey), the framework will re-initialize itself. During re-initialization, all configuration settings are re-applied and all cached objects are cleared and reloaded. If the value of the key does not match the reload password, a reload will not be performed. This allows you to set a secret password to restrict control of reloading your API to trusted parties.
#### reloadOnEveryRequest
**Available in:** Taffy 1.2+<br/>
**Type:** Boolean<br/>
**Default:** False<br/>
**Description:** Flag that indicates whether Taffy should reload cached values and configuration on every request. Useful in development; set to FALSE in production.
#### endpointURLParam
**Available in:** Taffy 1.3+<br/>
**Type:** String<br/>
**Default:** "endpoint"<br/>
**Description:** The query-string parameter name that can optionally be used to specify URI. Until now, URI formatting has been required to be index.cfm/URI; this parameter allows you to use index.cfm?endpoint=/URI. This setting (endpointURLParam) allows you to change the default parameter name of "endpoint" to something custom.
#### representationClass
**Available in:** Taffy 1.2+<br/>
**Type:** String<br/>
**Default:** "taffy.core.nativeJsonRepresentation"<br/>
**Description:** The CFC dot-notation path, or bean name, of the representation class that your API will use to serialize returned data for the client.
#### dashboardKey
**Available in:** Taffy 1.2+<br/>
**Deprecated **in: Taffy 1.3+<br/>
**Type:** String<br/>
**Default:** "dashboard"
Description: Name of the url parameter that displays the dashboard. The dashboard displays resources that your API is aware of, generates documentation about your API based on hint attributes, and contains a mock client to make testing your API easy.
#### disableDashboard
**Available in:** Taffy 1.2+<br/>
**Type:** Boolean<br/>
**Default:** False<br/>
**Description:** Whether or not Taffy will allow the dashboard to be displayed. If set to true, the dashboard key is simply ignored. You may wish to disable the dashboard in production, depending on whether or not you want customers/clients to be able to see it.
#### disabledDashboardRedirect
**Available in:** Taffy 1.3+<br/>
**Type:** String<br/>
**Default:** ""<br/>
**Description:** URL to which Taffy should redirect (302) the client/browser if the dashboard is disabled. If the dashboard is disabled and this value is blank, a simple 403 Forbidden response is sent instead.
#### jsonp
**Available in:** Taffy 2.0+<br/>
**Type:** Boolean/String<br/>
**Default:** false<br/>
**Description:** When false, JSONP is disabled. To enable it, change the value to a string, such as "callback". The value you specify will be the query parameter in which Taffy expects to find the JSONP callback name. Note: JSONP only works for GET requests (by design!)
#### unhandledPaths
**Available in:** Taffy 1.2+<br/>
**Type:** String (Comma-delimited list)<br/>
**Default:** "/flex2gateway"<br/>
**Description:** Set a list of paths (usually subfolders of the API) that you do not want Taffy to interfere with. Unless listed here, Taffy takes over the request lifecycle and does not execute the requested ColdFusion template.
#### allowCrossDomain
**Available in:** Taffy 1.2+<br/>
**Type:** Boolean<br/>
**Default:** False<br/>
**Description:** Whether or not to allow cross-domain access to your API. Turning this on adds the following headers:
```xml
<cfheader name="Access-Control-Allow-Origin" value="*" />
<cfheader name="Access-Control-Allow-Methods" value="#allowedVerbs#" />
<cfheader name="Access-Control-Allow-Headers" value="Content-Type" />
```
The allowed verbs, of course, are the ones allowed by the requested resource, as well as OPTIONS.
#### globalHeaders
**Available in:** Taffy 1.2+<br/>
**Type:** Structure<br/>
**Default:** {}<br/>
**Description:** A structure where each key is the name of a header you want to return, such as "X-MY-HEADER" and the structure value is the header value.
Global headers are static. You set them on application initialization and they do not change. If you need dynamic headers, you can add them to each response at runtime using withHeaders().
#### debugKey
**Available in:** Taffy 1.2+<br/>
**Type:** String<br/>
**Default:** "debug"<br/>
**Description:** Name of the url parameter that enables CF Debug Output.
#### useEtags
**Available in:** Taffy 1.3+<br/>
**Type:** Boolean<br/>
**Default:** False<br/>
**Description:** Enable the use of HTTP ETags for caching purposes. Taffy will automatically handle both sending the server ETag value and detecting client supplied ETags (via the If-None-Match header) for you; simply turn this setting on.
**NOTE FOR RAILO USERS:** While it will not cause errors, the underlying Java code used in this feature was improperly implemented prior to Railo 4.0.? and this could result in your result data being sent as if it were changed when it in fact has not. (I'm not sure which Railo point release will include the fix. The latest as of this writing is version 4.0.2, and does not include it.) Adobe ColdFusion is unaffected.
#### returnExceptionsAsJson
**Available in:** Taffy 1.2+<br/>
**Type:** Boolean<br/>
**Default:** true<br/>
**Description:** When an error occurs that is not otherwise handled, this option tells Taffy to attempt to format the error information as JSON and return that (regardless of the requested return format). As of Taffy 2.1 this also includes a structured stack trace with file names and line numbers.
#### exceptionLogAdapter
**Available in:** Taffy 1.2+<br/>
**Type:** String<br/>
**Default:** "taffy.bonus.LogToEmail"<br/>
**Description:** CFC dot-notation path to the exception logging adapter you want to use. Default adapter simply emails all exceptions. See [Exception Logging Adapters](https://github.com/atuttle/Taffy/wiki/Exception-Logging-Adapters) for more details.
#### exceptionLogAdapterConfig
**Available in:** Taffy 1.2+<br/>
**Type:** Any<br/>
**Default:** See defaults [here](#variablesframework-settings)<br/>
**Description:** Configuration that your chosen logging adapter requires. Can be any data type. See [Exception Logging Adapters](https://github.com/atuttle/Taffy/wiki/Exception-Logging-Adapters) for more details.
#### beanFactory
**Available in:** Taffy 1.2+<br/>
**Type:** Object Instance<br/>
**Default:** ""<br/>
**Description:** Already instantiated and cached (e.g. in Application scope) object instance of your external bean factory. Not required in order to use Taffy's built-in factory.
#### environments
**Available in:** Taffy 1.3+<br/>
**Type:** Structure<br/>
**Default:** {}<br/>
**Description:** Environment-specific overrides to any framework settings. Applied after general variables.framework settings, and after configureTaffy() has been called. See [getEnvironment()](#getenvironment) for more details.
## Index of API Methods
### Application.cfc
#### getPath()
**Parameters:** _(none)_
This method is provided as an extension point. On Adobe ColdFusion ("ACF") 9, installed in standard entire-server mode, no change should be necessary. However, if ACF is installed on another JEE app server (i.e. Tomcat, Glassfish, etc), or on JRun but using an EAR/WAR setup, then you may need to override this method to make Taffy work on your server.
#### getBasicAuthCredentials()
**Parameters:** _(none)_
**Added in Taffy 1.3.** This method returns a structure with two keys: `username`, and `password`. When the client does not provide HTTP Basic Auth credentials, the username and password keys will be blank. When the client does provide them, the values will be available in these keys.
This method is only available inside your Application.cfc. If, for example, you need access to the username in a resource, you can use this method inside onTaffyRequest and add the username to the requestArguments structure.
#### getBeanFactory()
**Parameters:** _(none)_
Returns whatever bean factory you may have set into Taffy, if any. If using the `/resources` folder, it returns Taffy's built-in factory.
#### newRepresentation()
**Use it inside:** onTaffyRequest<br/>
**Parameters:**
- **class (string)** - the dot-notation cfc path (or bean id if managing the class with a bean factory, including the built-in factory) for the representation class. Optional. Default value is the defined representation class. If you're using the framework default representation class, it's `nativeJsonRepresentation`.
Use this method inside onTaffyRequest when you want to _abort_ the request with some specific message or data. Use the `setData` method on the returned representation class to put your data/message into it before returning it.
#### getEnvironment()
**Parameters:** _(none)_
Taffy calls this method during initialization to determine in which configured environment, if any, it is executing. Overriding it is optional. Use whatever methodology you like to determine the result (e.g. hostname, reading a file or registry setting, etc.), and simply return, as string, the name of the current environment.
The returned value will be used to load environment-specific configuration. For example, if you have the following code in your Application.cfc, then the dashboard will be disabled in production:
```js
variables.framework = {
disableDashboard = false
,environments = {
production = {
disableDashboard = true
}
}
};
function getEnvironment(){
if (...){
return "production";
}else{
return "development";
}
}
```
#### getExternalBeanFactory()
**Parameters:** _(none)_
Returns the external bean factory you have set into Taffy as `variables.framework.beanFactory`. Unlike `getBeanFactory()` this method always returns the external factory, even if using the `/resources` folder (which causes Taffy to use its internal Factory as well).
#### onTaffyRequest()
**Parameters:**
- **verb (string)** - The HTTP request verb provided by the client
- **cfc (string)** - The CFC name (minus ".cfc") that would handle the request. (Bean Name, if using an external bean factory.)
- **requestArguments (struct)** - A structure containing all of the arguments of the request, including tokens from the URI as well as any query string parameters.
- **mimeExt (string)** - The mime extension (e.g. "json" - NOT the full mime type, e.g. "application/json")
- **headers (struct)** - A structure containing each header from the request, as sent by the client.
- **methodMetadata (struct)** - A structure containing any non-taffy metadata set on the requested resource method.
This method is optional, and allows you to inspect and potentially abort an API request in a way that adheres to the HTTP specification and REST best practices. If you choose not to override it (by implementing it in your Application.cfc), it will always return true, allowing the request to continue. If you implement it, you can check for things like an API key, or whether or not the customer has paid for your service, and return something other than the data that they are requesting.
If you do not return TRUE, allowing the request to continue as normal, then Taffy expects you to return a **[representation](#custom-representation-classes)** (either the default class, or a custom one) that it should immediately return to the consumer, serialized to the appropriate format. If you simply want to return with a status code of 403 (which indicates "Not Allowed"), you could do this:
```js
return newRepresentation().noData().withStatus(403);
```
Alternately, you could return some data to indicate that they owe you money or something:
```js
return newRepresentation()
.setData({error="Your account is past due. Please email accounts payable."})
.withStatus(403);
```
The options here are limited only by your imagination.
You can add data to the **requestArguments** structure and this will be passed on to any resource that handles the request. Simply add a key to the structure:
```js
function onTaffyRequest(verb, cfc, requestArguments, mimeExt, headers) {
arguments.requestArguments.myData = "myvalue";
return true;
}
```
In your resource:
```js
function get(myData) {
//arguments.myData => "myValue"
}
```
You can use the method metadata for anything you see fit; but the original use case was for role-based security. Consider the following resource method:
```js
public function getData( id ) taffy_method="get" role="datareader" { ... }
```
Taffy doesn't do anything with the **role** metadata on this method other than expose it to you in onTaffyRequest. So let's use the user's API key to find out what roles they have, and verify that the method's required role is among them. This is a snippet from your Application.cfc:
```js
function onTaffyRequest(verb, cfc, requestArgs, mime, head, methodMetadata){
local.user = (...); //get user from api key...
if (structKeyExists(methodMetadata, "role")){
for (var availableRole in local.user.roles){
if (availableRole == methodMetadata.role) { return true; }
}
return newRepresentation().noData().withStatus(403, "Not Authorized");
}else{
//no role required on the method, so allow anyone to use it
return true;
}
}
```
Here, if the user doesn't have the `datareader` role assigned, they'll get a 403 response.
### Resources
Resource CFCs extend `taffy.core.resource`. The following methods are available inside each of your Resource CFCs:
#### noData()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:** _(none)_
This method allows you to specify that there is no data to be returned for the current request. Generally, you would use it in conjunction with the **withStatus** method to set a specific return status for the request. For example, if the requested resource doesn't exist, you could return a 404 error like so:
```js
return noData().withStatus(404);
```
#### queryToArray()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **data (query)** - The query object to be transformed
This method transforms a ColdFusion query object into an array of structures. It was added because ColdFusion's serializeJSON functionality uses an ..._eccentric_... format for queries. **queryToArray** returns the format most people expect: a vanilla array of structures with named keys. To be fair the ACF serialization format uses less data as long as there is more than 1 row in the query, but it doesn't matter that you do a better job if nobody understands your output. _queryToArray also preserves query column name case, which serializeJSON does not._
#### queryToStruct()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **data (query)** - The query object to be transformed
This method transforms a ColdFusion query object into a structure. If there is more than one row in the query, only the first row is used. This is a sort of shorthand syntax for `queryToArray(qry)[1]`, when you know there will only be one record and you want the structure, but not wrapped in an array. Like **queryToArray**, **queryToStruct** preserves query column casing in the structure key names.
#### representationOf()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **data (any)** - The data to return to the consumer.
Data can be of any type, including complex data types like queries, structures, and arrays, as long as the serializer knows how to serialize them. For more information on using a custom representation class, see [Using a custom Representation Class](https://github.com/atuttle/Taffy/wiki/Using-a-Custom-Representation-Class).
#### saveLog()
**Use it inside:** anywhere inside a Resource CFC to log data using your [configured logging adapter](https://github.com/atuttle/Taffy/wiki/Exception-Logging-Adapters).<br/>
**Parameters:**
- **exception (struct)** - traditionally a CF exception object, but any struct may be passed.
What you pass to this method is simply handed off to the logging adapter. You may use one of the included adapters (LogToEmail, LogToBuglogHQ, or LogToHoth), or a custom logging adapter. If you write a custom logging adapter, it should implement the `taffy.bonus.ILogAdapter` interface.
If you don't configure a logging adapter, the default is LogToEmail, but the default `from` and `to` email addresses are not useful. See [Exception Log Adapters](https://github.com/atuttle/Taffy/wiki/Exception-Logging-Adapters) for more information on configuring logging adapters.
#### streamBinary()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **binaryData (any)** - the binary data to be streamed back to the consumer
Use this method in place of `representationOf()` to return a stream of binary data. Useful for streaming things like dynamically generated PDFs.
#### streamFile()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **fileName (string)** - fully qualified file path (eg c:\tmp\files.zip)
Use this method in place of `representationOf()` to stream a file from disk (or VFS). Optionally append `.andDelete( true )` to delete the file once streaming is complete.
```js
return streamFile("/foo.txt").andDelete(true);
```
#### streamImage()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **binaryData (any)** - the binary, base64 encoded data of the image, to be streamed back to the consumer
Use this method in place of `representationOf()` to stream an image from disk (or VFS).
#### withHeaders()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **headerStruct (struct)** - A structure whose keys are desired header names ("x-powered-by") and whose associated values are the values for the corresponding headers ("Taffy 1.1!").
This special method _**requires**_ the use of either **noData** or **representationOf**. It adds custom headers to the return. Additional use of **withStatus** optional.
```js
return representationOf(myData).withHeaders({"X-POWERED-BY"="Taffy 2.0!"});
```
#### withMime()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **mime (string)** - mime type (eg. "application/pdf") to be returned with the streamed file data
This special method _**requires**_ the use of either **streamFile** or **streamBinary**. It overrides the default mime type header for the return.
```js
return streamFile("kittens/cuteness.pdf").withMime("application/pdf");
```
#### withStatus()
**Use it inside:** responder methods inside your Resource CFCs (e.g. get, put, post, delete).<br/>
**Parameters:**
- **statusCode (numeric)** - the [HTTP Status Code](http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html) to return to the consumer.
- **statusText (string)** - the Status Text to include with the HTTP Status Code. The "OK" part of "200 OK"; the "Not Found" part of "404 Not Found".
This special method _**requires**_ the use of either **noData** or **representationOf**. It sets the HTTP Status Code of the return. Additional use of **withHeaders** optional.
_If you do not specify a return status code, Taffy will always return status code 200 (OK) by default._
```js
return noData().withStatus(404, "Not Found");
```
### Custom Representation Classes
Representation Classes have the job of converting the native data that your resources return (queries, structures, arrays, and so on...) into &mdash;usually&mdash; a string that can be sent to the client and used there. It's also possible to send images and binary files like PDFs and ZIPs, but that's a topic that has its own guide. Taffy comes with a representation class that uses ColdFusion's **serializeJson** method to convert native data to JSON strings, and uses this class by default.
_Why would you want to create a custom representation class, and how would you do it?_
#### Why
You want to create a Custom Representation Class ("CRC") if you're stymied by [the bugs in ColdFusion's serializeJson method](http://fusiongrokker.com/post/you-can-help-fix-coldfusion-json-serialization), or if you want to return something other than JSON: XML for example. Another reason to use a CRC is when your api should be capable of returning more than one return format &mdash; e.g. both XML and JSON.
#### How
A Custom Representation Class is a CFC that:
- Extends `taffy.core.baseRepresentation`
- Implements 1 or more "getAs{something}" methods
- ... that defines a `taffy:mime` attribute
Let's take a look at **taffy.core.nativeJsonRepresentation** &mdash; the default representation class that Taffy uses unless you configure something else.
```xml
<cfcomponent extends="taffy.core.baseRepresentation" output="false">
<cffunction
name="getAsJson"
output="false"
taffy:mime="application/json"
taffy:default="true"
hint="serializes data as JSON">
<cfreturn serializeJSON(variables.data) />
</cffunction>
</cfcomponent>
```
- Extends `taffy.core.baseRepresentation`? Check.
- Implements 1 or more "getAs{something}" methods? Check.
- ... that defines a `taffy:mime` attribute? Check.
The base class is easy to explain: It makes everything about CRC's work without you having to do anything. It's the black magic.
What's the `getAs{something}` method for? Well, it's for **get**ting the data out of your CRC in a format that the user is requesting. The client can specify its desired return format in one of two ways. An HTTP `Accept` header, or by appending `.{format}` to the URL.
The former looks like: `Accept: application/json`, and the latter looks like:
```
http://example.com/api/v1/index.cfm/users.json
```
You may be familiar or comfortable with either, but the point is that they both mean the same thing. Taffy looks at the methods in your CRC at startup to determine what types your API will support. It needs both the format-extension (.json, via `getAsJson`) and the mime type (application/json, via taffy:mime) in order to function properly.
When writing your own CRC, you can assume that a resource has returned data and the returned value is available to the CRC as `variables.data`. This is why the default class returns `serializeJson(variables.data)`. If your method is `getAsXML` then it needs to be able to convert the data into an XML string.
#### Multiple Formats
Since a common use-case for CRCs is supporting multiple data formats, we'll next look at **CustomRepresentationClass.cfc**, found in Taffy's **examples/api_twoFormats/resources** folder.
```xml
<cfcomponent extends="taffy.core.baseRepresentation">
<cfset variables.jsonUtil = application.jsonUtil />
<cfset variables.AnythingToXML = application.AnythingToXML />
<cffunction name="getAsJSON" taffy:mime="application/json" taffy:default="true" output="false">
<cfreturn variables.jsonUtil.serializeJson(variables.data) />
</cffunction>
<cffunction name="getAsXML" taffy:mime="application/xml" output="false">
<cfreturn variables.AnythingToXML.ToXML(variables.data) />
</cffunction>
</cfcomponent>
```
Here we see two getters: `getAsJSON` with `taffy:mime="application/json"` and `getAsXML` with `taffy:mime="application/xml"`. Each is using a 3rd party library (available in the example folder) to do the serialization work. It's assuming that these libraries are available in Application scope (check Application.cfc for the implementation). This is generally regarded as a bad practice ("tight coupling"), and Taffy does not require that your code be structured this way. It's only included this way to keep the example as simple and focused as possible.
When a request is made that wants XML back, Taffy calls the `getAsXML` method to get the XML string. When a request is made for JSON data, Taffy calls `getAsJSON`.
And that &mdash;in a nutshell&mdash; is Custom Representation Classes.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+44
View File
@@ -0,0 +1,44 @@
<style>
* {
line-height: 1.2;
margin: 0;
}
html {
color: #888;
display: table;
font-family: sans-serif;
height: 100%;
text-align: center;
width: 100%;
}
body {
display: table-cell;
vertical-align: middle;
margin: 2em auto;
}
h1 {
color: #555;
font-size: 2em;
font-weight: 400;
}
p {
margin: 0 auto;
width: 440px;
}
@media only screen and (max-width: 280px) {
body, p {
width: 95%;
}
h1 {
font-size: 1.5em;
margin: 0 0 0.3em;
}
}
</style>
<iframe src="http://gfycat.com/ifr/LoathsomeSaltyAssassinbug" frameborder="0" scrolling="no" width="380" height="286" style="-webkit-backface-visibility: hidden;-webkit-transform: scale(1);" ></iframe>
# 404
The page you're looking for could not be found. If you followed a link to get here, [please report that link as incorrect](https://github.com/atuttle/TaffyDocs/issues)!
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
docs.taffy.io
Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM "http://www.adobe.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
<!-- Read this: https://www.adobe.com/devnet/articles/crossdomain_policy_file_spec.html -->
<!-- Most restrictive policy: -->
<site-control permitted-cross-domain-policies="none"/>
<!-- Least restrictive policy: -->
<!--
<site-control permitted-cross-domain-policies="all"/>
<allow-access-from domain="*" to-ports="*" secure="false"/>
<allow-http-request-headers-from domain="*" headers="*" secure="false"/>
-->
</cross-domain-policy>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

+47
View File
@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Document</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta
name="description"
content="Documentation for Taffy — The REST Web Service Framework for ColdFusion and Lucee"
/>
<meta
name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
/>
<link
rel="stylesheet"
href="//cdn.jsdelivr.net/npm/docsify/lib/themes/buble.css"
/>
</head>
<body>
<div id="app"></div>
<aside
class="book"
style="position: absolute; top: 50px; right: 20px; width: 160px"
>
<br />Need to better understand REST fundamentals? <br /><a
href="http://www.restassuredbook.com"
>
<img
src="assets/book-3d.png"
width="100"
alt="Picture of book cover art for Adam's book: REST Assured, A Pragmatic Approach to API Design"
/>
<br />I wrote a book for you.
</a>
</aside>
<script>
window.$docsify = {
name: "Taffy Docs",
repo: "atuttle/Taffy",
maxLevel: 4,
};
</script>
<script src="//cdn.jsdelivr.net/npm/docsify@4"></script>
<script src="//cdn.jsdelivr.net/npm/docsify/lib/plugins/emoji.min.js"></script>
</body>
</html>
+32
View File
@@ -0,0 +1,32 @@
<img alt="Taffy Logo" src="https://taffy.io/images/logo.png" style="max-width: 300px" />
# Taffy
The REST Web Service framework for ColdFusion and Lucee
- [Download Taffy 3.3.0.zip](https://github.com/atuttle/Taffy/archive/v3.3.0.zip)
- [GitHub](https://github.com/atuttle/Taffy)
- Join us in the **#taffy** chat room on the [CFML Slack](https://cfml-slack.herokuapp.com/)
# Documentation
Nobody has time to keep every framework and every dependency up to date. Sometimes you want to reference the old docs! That's why Taffy tries really hard to maintain our old docs and carry them forward with us. Below is a list of all version-specific Taffy docs releases.
<!--new_docs_links_here-->
- [v3.5.0](3.5.0.md)
- [v3.4.0](3.4.0.md)
- [v3.3.0](3.3.0.md)
- [v3.2.0](3.2.0.md)
- [v3.1.0](3.1.0.md)
- [v3.0.0](3.0.0.md)
- [v2.2.4](2.2.4.md)
- [v2.2.3](2.2.3.md)
- [v2.2.0](2.2.0.md)
- [v2.1.0](2.1.0.md)
- [v2.0.1](2.0.1.md)
- [v2.0.0](2.0.0.md)
# Other Resources
In 2012 I presented a comparison of REST frameworks for CFML at the cf.Objective() conference. [This is all of the source code I used to make that comparison, as well as my slides from the talk.](https://github.com/atuttle/CF-REST-Comparison#coldfusion-rest-comparison)
+2
View File
@@ -0,0 +1,2 @@
User-agent: *
Disallow:
@@ -0,0 +1,26 @@
<cfcomponent output="false">
<cfscript>
this.name = "taffy_ParentAppExample";//same name as parent folder application.cfc
this.applicationTimeout = createTimeSpan(0,2,0,0);
this.sessionManagement = false;
this.setClientCookies = false;
this.scriptProtect = false;
</cfscript>
<cffunction name="onApplicationStart" returnType="boolean" output="false">
<cfinclude template="mixin/appInit.cfm" />
<cfreturn true />
</cffunction>
<cffunction name="onRequestStart" returnType="boolean" output="false">
<cfargument name="thePage" type="string" required="true" />
<cfif structKeyExists(url, "reinit")>
<cfset onApplicationStart() />
</cfif>
<cfreturn true />
</cffunction>
</cfcomponent>
@@ -0,0 +1,6 @@
RewriteEngine On
#if we've already been rewritten to use index.cfm/* format, don't make a circular request
RewriteRule index\.cfm/* - [L]
RewriteRule ^(.*)$ index.cfm/$1
@@ -0,0 +1,16 @@
component extends="taffy.core.api" {
this.name = "taffy_ParentAppExample";//same name as api folder application.cfc
variables.framework = {};
variables.framework.beanFactory = "";
function onApplicationStart(){
include "../mixin/appInit.cfm";
variables.framework.beanFactory = application.beanFactory;
return super.onApplicationStart();
}
}
@@ -0,0 +1,6 @@
<!---
DO NOT DELETE THIS FILE
===============================
This file is necessary for Taffy to work, but should be blank.
All requests funnel through it, but are handled by framework internal functionality.
--->
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<bean id="fakeData" class="taffy.examples.shared.fakeData" />
<bean id="fooCollection" class="taffy.examples.ParentApplication.model.fooCollection">
<property name="fakeData">
<ref bean="fakeData" />
</property>
</bean>
</beans>
@@ -0,0 +1,5 @@
<h1>Parent Application</h1>
<p>Hi there. I'm the parent application. Want to see what's under the hood?</p>
<cfdump var="#application#">
@@ -0,0 +1,2 @@
<cfset application.beanFactory = createObject("component", "coldspring.beans.DefaultXMLBeanFactory") />
<cfset application.beanFactory.loadBeans('/taffy/examples/ParentApplication/config/coldspring.xml') />
@@ -0,0 +1,13 @@
<cfcomponent extends="taffy.core.resource" taffy_uri="/foo">
<cffunction name="get" access="public" output="false">
<cfreturn representationOf(variables.fakeData.getData()).withStatus(200) />
</cffunction>
<!--- this will be called by the bean factory's autowire functionality --->
<cffunction name="setFakeData" access="public" output="false" returnType="void">
<cfargument name="fakeDataObj" type="any" required="true" hint="Shared FakeData object" />
<cfset variables.fakeData = arguments.fakeDataObj />
</cffunction>
</cfcomponent>
+6
View File
@@ -0,0 +1,6 @@
RewriteEngine On
#if we've already been rewritten to use index.cfm/* format, don't make a circular request
RewriteRule index\.cfm/* - [L]
RewriteRule ^(.*)$ index.cfm/$1
+30
View File
@@ -0,0 +1,30 @@
<cfcomponent extends="taffy.core.api">
<cfscript>
this.name = hash(getCurrentTemplatePath());
this.mappings["/resources"] = listDeleteAt(cgi.script_name, listLen(cgi.script_name, "/"), "/") & "/resources";
variables.framework = {};
variables.framework.debugKey = "debug";
variables.framework.reloadKey = "reload";
variables.framework.reloadPassword = "true";
variables.framework.serializer = "taffy.core.nativeJsonSerializer";
variables.framework.returnExceptionsAsJson = true;
function onApplicationStart(){
return super.onApplicationStart();
}
function onRequestStart(TARGETPATH){
return super.onRequestStart(TARGETPATH);
}
// this function is called after the request has been parsed and all request details are known
function onTaffyRequest(verb, cfc, requestArguments, mimeExt){
// this would be a good place for you to check API key validity and other non-resource-specific validation
return true;
}
</cfscript>
</cfcomponent>
+6
View File
@@ -0,0 +1,6 @@
<!---
DO NOT DELETE THIS FILE
===============================
This file is necessary for Taffy to work, but should be blank.
All requests funnel through it, but are handled by framework internal functionality.
--->
@@ -0,0 +1,24 @@
<cfcomponent extends="taffy.core.resource" taffy_uri="/artist/{artistId}/art/{artId}">
<cfset variables.dummyData = StructNew() />
<cfset variables.dummyData.whatever = true />
<cfset variables.dummyData.phone = encode.string(5558675309) />
<cfset variables.dummyData.phoneNumeric = 5558675309 />
<cffunction name="get" access="public" output="false">
<cfreturn representationOf(variables.dummyData).withStatus(200) />
</cffunction>
<cffunction name="post" access="public" output="false">
<cfreturn representationOf(variables.dummyData).withStatus(200) />
</cffunction>
<cffunction name="put" access="public" output="false">
<cfreturn representationOf(variables.dummyData).withStatus(200) />
</cffunction>
<cffunction name="delete" access="public" output="false">
<cfreturn representationOf(variables.dummyData).withStatus(200) />
</cffunction>
</cfcomponent>
@@ -0,0 +1,84 @@
<cfcomponent extends="taffy.core.resource" taffy:uri="/artist/{id}" hint="some hint about this resource">
<cffunction name="get" access="public" output="false">
<cfargument name="id" type="numeric" required="true" />
<cfset var q = ""/>
<cfset var col = "" />
<cfset var rtn = StructNew() />
<cfquery name="q" datasource="cfartgallery">
select * from artists where artistId = <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.id#" />
</cfquery>
<cfif q.recordCount gt 0>
<cfloop list="#q.ColumnList#" index="col">
<cfset rtn[col] = q[col][1] />
</cfloop>
<cfreturn representationOf(rtn).withStatus(200) />
<cfelse>
<cfreturn noData().withStatus(404) />
</cfif>
</cffunction>
<cffunction name="put" access="public" output="false">
<cfargument name="id" type="numeric" required="true" />
<cfargument name="firstname" type="string" required="false" default="" />
<cfargument name="lastname" type="string" required="false" default="" />
<cfargument name="address" type="string" required="false" default="" hint="some hint about this parameter" />
<cfargument name="city" type="string" required="false" default="" />
<cfargument name="state" type="string" required="false" default="" />
<cfargument name="postalcode" type="string" required="false" default="" />
<cfargument name="email" type="string" required="false" default="" />
<cfargument name="phone" type="string" required="false" default="" />
<cfargument name="fax" type="string" required="false" default="" />
<cfargument name="thepassword" type="string" required="false" default="" />
<cfset var q = "" />
<cfquery name="q" datasource="cfartgallery">
update artists
set artistid=artistid
<cfif len(arguments.firstname)>
,firstname = <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.firstname#" />
</cfif>
<cfif len(arguments.lastname)>
,lastname = <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.lastname#" />
</cfif>
<cfif len(arguments.address)>
,address = <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.address#" />
</cfif>
<cfif len(arguments.city)>
,city = <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.city#" />
</cfif>
<cfif len(arguments.state)>
,state = <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.state#" />
</cfif>
<cfif len(arguments.postalcode)>
,postalcode = <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.postalcode#" />
</cfif>
<cfif len(arguments.email)>
,email = <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.email#" />
</cfif>
<cfif len(arguments.phone)>
,phone = <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.phone#" />
</cfif>
<cfif len(arguments.fax)>
,fax = <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.fax#" />
</cfif>
<cfif len(arguments.thepassword)>
,thepassword = <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.thepassword#" />
</cfif>
where artistid = <cfqueryparam cfsqltype="cf_sql_numeric" value="#arguments.id#" />
</cfquery>
<cfreturn noData().withStatus(200) />
</cffunction>
<cffunction name="delete" access="public" output="false">
<cfargument name="id" type="numeric" required="true" />
<cfset var q = "" />
<cfquery name="q" datasource="cfartgallery">
delete from artists where artistid = <cfqueryparam cfsqltype="cf_sql_numeric" value="#arguments.id#" />
</cfquery>
<cfreturn noData().withStatus(200) />
</cffunction>
<cffunction name="someInternalMethod" hint="you should not see this documentation...">
</cffunction>
</cfcomponent>
@@ -0,0 +1,15 @@
<cfcomponent extends="taffy.core.resource" taffy:uri="/artist/{artistId}/art" hint="Collection of art data">
<cfset variables.dummyData = StructNew() />
<cfset variables.dummyData.whatever = true />
<cffunction name="get" access="public" output="false" hint="Get some collection of art data">
<cfargument name="artistId" type="numeric" required="true" />
<cfreturn representationOf(variables.dummyData).withStatus(200) />
</cffunction>
<cffunction name="post" access="public" output="false" hint="Insert a new art record">
<cfreturn representationOf(variables.dummyData).withStatus(200) />
</cffunction>
</cfcomponent>
@@ -0,0 +1,59 @@
<cfcomponent extends="taffy.core.resource" taffy_uri="/artists">
<cffunction name="get" access="public" output="false">
<cfset var q = "" />
<cfquery name="q" datasource="cfartgallery">
select * from artists
</cfquery>
<cfreturn representationOf(q).withStatus(200) />
</cffunction>
<cffunction name="post" access="public" output="false">
<cfargument name="firstname" type="string" required="false" default="" />
<cfargument name="lastname" type="string" required="false" default="" />
<cfargument name="address" type="string" required="false" default="" />
<cfargument name="city" type="string" required="false" default="" />
<cfargument name="state" type="string" required="false" default="" />
<cfargument name="postalcode" type="string" required="false" default="" />
<cfargument name="email" type="string" required="false" default="" />
<cfargument name="phone" type="string" required="false" default="" />
<cfargument name="fax" type="string" required="false" default="" />
<cfargument name="thepassword" type="string" required="false" default="" />
<cfset var q = "" />
<cfquery name="q" datasource="cfartgallery">
insert into artists (firstname,lastname,address,city,state,postalcode,email,phone,fax,thepassword)
values (
<cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.firstname#" />,
<cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.lastname#" />,
<cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.address#" />,
<cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.city#" />,
<cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.state#" />,
<cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.postalcode#" />,
<cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.email#" />,
<cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.phone#" />,
<cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.fax#" />,
<cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.thepassword#" />
)
</cfquery>
<cfquery name="q" datasource="cfartgallery">
select * from artists
where
firstname = <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.firstname#" />
and lastname = <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.lastname#" />
and address = <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.address#" />
and city = <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.city#" />
and state = <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.state#" />
and postalcode = <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.postalcode#" />
and email = <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.email#" />
and phone = <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.phone#" />
and fax = <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.fax#" />
and thepassword = <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.thepassword#" />
</cfquery>
<cfreturn representationOf(q).withStatus(200) />
</cffunction>
<!---
The DELETE and PUT verbs are not implemented, so those actions are not permitted.
--->
</cfcomponent>
@@ -0,0 +1,18 @@
<cfcomponent extends="taffy.core.api">
<cfscript>
this.name = hash(getCurrentTemplatePath());
variables.framework = {};
variables.framework.exceptionLogAdapter = "taffy.bonus.LogToBugLogHQ";
variables.framework.exceptionLogAdapterConfig = StructNew();
variables.framework.exceptionLogAdapterConfig.bugLogListener = "bugLog.listeners.bugLogListenerWS";
variables.framework.exceptionLogAdapterConfig.bugEmailRecipients = "you@yourdomain.com";
variables.framework.exceptionLogAdapterConfig.bugEmailSender = "errors@yourdomain.com";
variables.framework.exceptionLogAdapterConfig.hostname = "Taffy_DEV_Examples";
variables.framework.exceptionLogAdapterConfig.apikey = "";
</cfscript>
</cfcomponent>
@@ -0,0 +1,7 @@
<cfcomponent extends="taffy.core.resource" taffy_uri="/foo">
<cffunction name="get" access="public" output="false">
<cfthrow message="this is the message" detail="this is the detail" />
</cffunction>
</cfcomponent>
+17
View File
@@ -0,0 +1,17 @@
<cfcomponent extends="taffy.core.api">
<cfscript>
this.name = hash(getCurrentTemplatePath());
variables.framework = {};
function onApplicationStart(){
application.beanFactory = createObject("component", "di1.ioc").init( "/taffy/examples/api_DI1/model" );
variables.framework.beanFactory = application.beanFactory;
return super.onApplicationStart();
}
</cfscript>
</cfcomponent>
View File
+7
View File
@@ -0,0 +1,7 @@
<cfcomponent extends="taffy.core.resource" taffy:uri="/foo">
<cffunction name="get">
<cfreturn representationOf("hi").withStatus(200) />
</cffunction>
</cfcomponent>
@@ -0,0 +1,10 @@
<cfcomponent extends="taffy.core.api">
<cfscript>
this.name = hash(getCurrentTemplatePath());
variables.framework = {};
variables.framework.exceptionLogAdapter = "taffy.bonus.LogToHoth";
variables.framework.exceptionLogAdapterConfig = "taffy.examples.api_hoth.resources.HothConfig";
</cfscript>
</cfcomponent>
@@ -0,0 +1 @@
Hoth: The files within this directory contain the complete details for each unique exception.
@@ -0,0 +1 @@
Hoth: The files within this directory contain the details about the volume of errors for each unique exception.
+6
View File
@@ -0,0 +1,6 @@
<!---
DO NOT DELETE THIS FILE
===============================
This file is necessary for Taffy to work, but should be blank.
All requests funnel through it, but are handled by framework internal functionality.
--->
@@ -0,0 +1,57 @@
component
implements = 'Hoth.object.iHothConfig'
extends = 'Hoth.object.CoreConfig'
accessors = 'true'
{
/** What is the name of your application? */
property
name='applicationName'
default='Taffy Tests';
/** How many seconds should we lock file operations?
For most operations this is exclusive to a unique exception. */
property
name='timeToLock'
default='4';
/** Where would you like Hoth to save exception data?
This folder should be empty. */
property
name='logPath'
default='/taffy/examples/api_hoth/hoth/exceptions';
// ------------------------------------------------------------------------------
/** Would you like new exceptions to be emailed to you? */
property
name='EmailNewExceptions'
default='true';
/** What address(es) should receive these e-mails? */
property
name='EmailNewExceptionsTo'
default='you@yourdomain.com';
/** What address would you like these emails sent from? */
property
name='EmailNewExceptionsFrom'
default='hoth-error-report@yourdomain.com';
/** Would you like the raw JSON attached to the e-mail? */
property
name='EmailNewExceptionsFile'
default='true';
// ------------------------------------------------------------------------------
/**
The mapping where you would like Hoth to write it's log files.
Without this setting, Hoth will write log files to the same directory
Hoth is located within. This is not recomended as your will have content
mixed into your Hoth code.
**/
setGlobalDatabasePath(path='/taffy/examples/api_hoth/hoth/logs/');
//make the log path be evaluated as relative
variables.logPathIsRelative = true;
}
@@ -0,0 +1,7 @@
<cfcomponent extends="taffy.core.resource" taffy_uri="/foo">
<cffunction name="get" access="public" output="false">
<cfthrow message="this is the message" detail="this is the detail" />
</cffunction>
</cfcomponent>
@@ -0,0 +1,17 @@
<cfcomponent extends="taffy.core.api">
<cfscript>
this.name = hash(getCurrentTemplatePath());
variables.framework = {};
variables.framework.exceptionLogAdapter = "taffy.bonus.LogToEmail";
variables.framework.exceptionLogAdapterConfig = StructNew();
variables.framework.exceptionLogAdapterConfig.emailFrom = "api-error@yourdomain.com";
variables.framework.exceptionLogAdapterConfig.emailTo = "you@yourdomain.com";
variables.framework.exceptionLogAdapterConfig.emailSubj = "Exception Trapped in API";
variables.framework.exceptionLogAdapterConfig.emailType = "html";
</cfscript>
</cfcomponent>
@@ -0,0 +1,10 @@
<cfcomponent extends="taffy.core.resource" taffy_uri="/foo">
<cffunction name="get" access="public" output="false">
<cfset local.exception = {} />
<cfset local.exception.message = "testing log to email from resource saveLog()" />
<cfset saveLog(local.exception) />
<cfreturn noData().withStatus(200, "Log Saved") />
</cffunction>
</cfcomponent>
@@ -0,0 +1,6 @@
RewriteEngine On
#if we've already been rewritten to use index.cfm/* format, don't make a circular request
RewriteRule index\.cfm/* - [L]
RewriteRule ^(.*)$ index.cfm/$1

Some files were not shown because too many files have changed in this diff Show More